diff mupdf-source/source/fitz/list-device.c @ 3:2c135c81b16c

MERGE: upstream PyMuPDF 1.26.4 with MuPDF 1.26.7
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:44:09 +0200
parents b50eed0cc0ef
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/source/fitz/list-device.c	Mon Sep 15 11:44:09 2025 +0200
@@ -0,0 +1,2126 @@
+// 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 <assert.h>
+#include <string.h>
+
+#define STACK_SIZE 96
+
+typedef enum
+{
+	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_RENDER_FLAGS,
+	FZ_CMD_DEFAULT_COLORSPACES,
+	FZ_CMD_BEGIN_LAYER,
+	FZ_CMD_END_LAYER,
+	FZ_CMD_BEGIN_STRUCTURE,
+	FZ_CMD_END_STRUCTURE,
+	FZ_CMD_BEGIN_METATEXT,
+	FZ_CMD_END_METATEXT
+} fz_display_command;
+
+/* The display list is a list of nodes.
+ * Each node is a structure consisting of a bitfield (that packs into a
+ * 32 bit word).
+ * The different fields in the bitfield identify what information is
+ * present in the node.
+ *
+ *	cmd:	What type of node this is.
+ *
+ *	size:	The number of sizeof(fz_display_node) bytes that this node's
+ *		data occupies. (i.e. &node[size] = the next node in the
+ *		chain; 0 for end of list).
+ *
+ *		At 9 bits for this field, and 4 bytes for a node, this means
+ *		the largest node can span 511*4 bytes. We therefore reserve
+ *		511 as a special value to mean that the actual size for the
+ *		node is given in an (unaligned!) size_t following the node
+ *		before the rest of the data.
+ *
+ *	rect:	0 for unchanged, 1 for present.
+ *
+ *	path:	0 for unchanged, 1 for present.
+ *
+ *	cs:	0 for unchanged
+ *		1 for devicegray (color defaults to 0)
+ *		2 for devicegray (color defaults to 1)
+ *		3 for devicergb (color defaults to 0,0,0)
+ *		4 for devicergb (color defaults to 1,1,1)
+ *		5 for devicecmyk (color defaults to 0,0,0,0)
+ *		6 for devicecmyk (color defaults to 0,0,0,1)
+ *		7 for present (color defaults to 0)
+ *
+ *	color:	0 for unchanged color, 1 for present.
+ *
+ *	alpha:	0 for unchanged, 1 for solid, 2 for transparent, 3
+ *		for alpha value present.
+ *
+ *	ctm:	0 for unchanged,
+ *		1 for change ad
+ *		2 for change bc
+ *		4 for change ef.
+ *
+ *	stroke:	0 for unchanged, 1 for present.
+ *
+ *	flags:	Flags (node specific meanings)
+ *
+ * Nodes are packed in the order:
+ * header, rect, colorspace, color, alpha, ctm, stroke_state, path, private data.
+ */
+typedef struct
+{
+	unsigned int cmd    : 5;
+	unsigned int size   : 9;
+	unsigned int rect   : 1;
+	unsigned int path   : 1;
+	unsigned int cs     : 3;
+	unsigned int color  : 1;
+	unsigned int alpha  : 2;
+	unsigned int ctm    : 3;
+	unsigned int stroke : 1;
+	unsigned int flags  : 6;
+} fz_display_node;
+
+enum {
+	CS_UNCHANGED = 0,
+	CS_GRAY_0    = 1,
+	CS_GRAY_1    = 2,
+	CS_RGB_0     = 3,
+	CS_RGB_1     = 4,
+	CS_CMYK_0    = 5,
+	CS_CMYK_1    = 6,
+	CS_OTHER_0   = 7,
+
+	ALPHA_UNCHANGED = 0,
+	ALPHA_1         = 1,
+	ALPHA_0         = 2,
+	ALPHA_PRESENT   = 3,
+
+	CTM_UNCHANGED = 0,
+	CTM_CHANGE_AD = 1,
+	CTM_CHANGE_BC = 2,
+	CTM_CHANGE_EF = 4,
+
+	INDIRECT_NODE_THRESHOLD = (1<<9)-1
+};
+
+struct fz_display_list
+{
+	fz_storable storable;
+	fz_display_node *list;
+	fz_rect mediabox;
+	size_t max;
+	size_t len;
+};
+
+typedef struct
+{
+	fz_device super;
+
+	fz_display_list *list;
+
+	fz_path *path;
+	float alpha;
+	fz_matrix ctm;
+	fz_stroke_state *stroke;
+	fz_colorspace *colorspace;
+	fz_color_params *color_params;
+	float color[FZ_MAX_COLORS];
+	fz_rect rect;
+
+	int top;
+	struct {
+		fz_rect *update;
+		fz_rect rect;
+	} stack[STACK_SIZE];
+	int tiled;
+} fz_list_device;
+
+enum { ISOLATED = 1, KNOCKOUT = 2 };
+enum { OPM = 1, OP = 2, BP = 3, RI = 4};
+
+#define SIZE_IN_NODES(t) \
+	((t + sizeof(fz_display_node) - 1) / sizeof(fz_display_node))
+
+/* The display list node are 32bit aligned. For some architectures we
+ * need to pad to 64bit for pointers. We allow for that here. */
+static void pad_size_for_pointer(const fz_display_list *list, size_t *size)
+{
+	/* list->len and size are both counting in nodes, not bytes.
+	 * Nodes are consistently 32bit things, hence we are looking
+	 * for even/odd for "8 byte aligned or not". */
+	if (FZ_POINTER_ALIGN_MOD <= 4)
+		return;
+	/* Otherwise, ensure we're on an even boundary. */
+	if (FZ_POINTER_ALIGN_MOD == 8)
+	{
+		if ((list->len + (*size)) & 1)
+			(*size)++;
+	} else
+		(*size) = ((list->len + (*size) + (FZ_POINTER_ALIGN_MOD>>2) - 1) & ~((FZ_POINTER_ALIGN_MOD>>2)-1)) - list->len;
+}
+
+static void align_node_for_pointer(fz_display_node **node)
+{
+	intptr_t ptr;
+
+	if (FZ_POINTER_ALIGN_MOD <= 4)
+		return;
+
+	ptr = (intptr_t)*node;
+	if (FZ_POINTER_ALIGN_MOD == 8)
+	{
+		if (ptr & 4)
+			(*node) = (fz_display_node *)(ptr+4);
+	}
+	else
+		(*node) = (fz_display_node *)((ptr + FZ_POINTER_ALIGN_MOD - 1) & ~(FZ_POINTER_ALIGN_MOD-1));
+}
+
+static int
+cmd_needs_alignment(fz_display_command cmd)
+{
+	return (cmd == FZ_CMD_FILL_TEXT ||
+		cmd == FZ_CMD_STROKE_TEXT ||
+		cmd == FZ_CMD_CLIP_TEXT ||
+		cmd == FZ_CMD_CLIP_STROKE_TEXT ||
+		cmd == FZ_CMD_IGNORE_TEXT ||
+		cmd == FZ_CMD_FILL_SHADE ||
+		cmd == FZ_CMD_FILL_IMAGE ||
+		cmd == FZ_CMD_FILL_IMAGE_MASK ||
+		cmd == FZ_CMD_CLIP_IMAGE_MASK ||
+		cmd == FZ_CMD_END_MASK ||
+		cmd == FZ_CMD_DEFAULT_COLORSPACES);
+}
+
+static unsigned char *
+fz_append_display_node(
+	fz_context *ctx,
+	fz_device *dev,
+	fz_display_command cmd,
+	int flags,
+	const fz_rect *rect,
+	const fz_path *path,
+	const float *color,
+	fz_colorspace *colorspace,
+	const float *alpha,
+	const fz_matrix *ctm,
+	const fz_stroke_state *stroke,
+	const void *private_data,
+	size_t private_data_len)
+{
+	fz_display_node node = { 0 };
+	fz_display_node *node_ptr;
+	fz_list_device *writer = (fz_list_device *)dev;
+	fz_display_list *list = writer->list;
+	size_t size;
+	size_t rect_off = 0;
+	size_t path_off = 0;
+	size_t color_off = 0;
+	size_t colorspace_off = 0;
+	size_t alpha_off = 0;
+	size_t ctm_off = 0;
+	size_t stroke_off = 0;
+	int rect_for_updates = 0;
+	size_t private_off = 0;
+	fz_path *my_path = NULL;
+	fz_stroke_state *my_stroke = NULL;
+	fz_rect local_rect;
+	size_t path_size = 0;
+	unsigned char *out_private = NULL;
+
+	switch (cmd)
+	{
+	case FZ_CMD_CLIP_PATH:
+	case FZ_CMD_CLIP_STROKE_PATH:
+	case FZ_CMD_CLIP_TEXT:
+	case FZ_CMD_CLIP_STROKE_TEXT:
+	case FZ_CMD_CLIP_IMAGE_MASK:
+		if (writer->top < STACK_SIZE)
+		{
+			rect_for_updates = 1;
+			writer->stack[writer->top].rect = fz_empty_rect;
+		}
+		writer->top++;
+		break;
+	case FZ_CMD_END_MASK:
+		if (writer->top < STACK_SIZE)
+		{
+			writer->stack[writer->top].update = NULL;
+			writer->stack[writer->top].rect = fz_empty_rect;
+		}
+		writer->top++;
+		break;
+	case FZ_CMD_BEGIN_TILE:
+		writer->tiled++;
+		if (writer->top > 0 && writer->top <= STACK_SIZE)
+		{
+			writer->stack[writer->top-1].rect = fz_infinite_rect;
+		}
+		break;
+	case FZ_CMD_END_TILE:
+		writer->tiled--;
+		break;
+	case FZ_CMD_END_GROUP:
+		break;
+	case FZ_CMD_POP_CLIP:
+		if (writer->top > STACK_SIZE)
+		{
+			writer->top--;
+			rect = &fz_infinite_rect;
+		}
+		else if (writer->top > 0)
+		{
+			fz_rect *update;
+			writer->top--;
+			update = writer->stack[writer->top].update;
+			if (writer->tiled == 0)
+			{
+				if (update)
+				{
+					*update = fz_intersect_rect(*update, writer->stack[writer->top].rect);
+					local_rect = *update;
+					rect = &local_rect;
+				}
+				else
+					rect = &writer->stack[writer->top].rect;
+			}
+			else
+				rect = &fz_infinite_rect;
+		}
+		/* fallthrough */
+	default:
+		if (writer->top > 0 && writer->tiled == 0 && writer->top <= STACK_SIZE && rect)
+			writer->stack[writer->top-1].rect = fz_union_rect(writer->stack[writer->top-1].rect, *rect);
+		break;
+	}
+
+	size = 1; /* 1 for the fz_display_node */
+	node.cmd = cmd;
+
+	/* Figure out what we need to write, and the offsets at which we will
+	 * write it. */
+	if (rect_for_updates || (rect != NULL && (writer->rect.x0 != rect->x0 || writer->rect.y0 != rect->y0 || writer->rect.x1 != rect->x1 || writer->rect.y1 != rect->y1)))
+	{
+		node.rect = 1;
+		rect_off = size;
+		size += SIZE_IN_NODES(sizeof(fz_rect));
+	}
+	if (color == NULL)
+	{
+		if (colorspace)
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "Colorspace cannot be specified without color.");
+	}
+	else
+	{
+		if (colorspace != writer->colorspace)
+		{
+			if (colorspace == fz_device_gray(ctx))
+			{
+				if (color[0] == 0.0f)
+					node.cs = CS_GRAY_0, color = NULL;
+				else
+				{
+					node.cs = CS_GRAY_1;
+					if (color[0] == 1.0f)
+						color = NULL;
+				}
+			}
+			else if (colorspace == fz_device_rgb(ctx))
+			{
+				if (color[0] == 0.0f && color[1] == 0.0f && color[2] == 0.0f)
+					node.cs = CS_RGB_0, color = NULL;
+				else
+				{
+					node.cs = CS_RGB_1;
+					if (color[0] == 1.0f && color[1] == 1.0f && color[2] == 1.0f)
+						color = NULL;
+				}
+			}
+			else if (colorspace == fz_device_cmyk(ctx))
+			{
+				node.cs = CS_CMYK_0;
+				if (color[0] == 0.0f && color[1] == 0.0f && color[2] == 0.0f)
+				{
+					if (color[3] == 0.0f)
+						color = NULL;
+					else
+					{
+						node.cs = CS_CMYK_1;
+						if (color[3] == 1.0f)
+							color = NULL;
+					}
+				}
+			}
+			else
+			{
+				int i;
+				int n = fz_colorspace_n(ctx, colorspace);
+
+				pad_size_for_pointer(list, &size);
+				colorspace_off = size;
+				size += SIZE_IN_NODES(sizeof(fz_colorspace *));
+				node.cs = CS_OTHER_0;
+				for (i = 0; i < n; i++)
+					if (color[i] != 0.0f)
+						break;
+				if (i == n)
+					color = NULL;
+				memset(writer->color, 0, sizeof(float)*n);
+			}
+		}
+		else
+		{
+			/* Colorspace is unchanged, but color may have changed
+			 * to something best coded as a colorspace change */
+			if (colorspace == fz_device_gray(ctx))
+			{
+				if (writer->color[0] != color[0])
+				{
+					if (color[0] == 0.0f)
+					{
+						node.cs = CS_GRAY_0;
+						color = NULL;
+					}
+					else if (color[0] == 1.0f)
+					{
+						node.cs = CS_GRAY_1;
+						color = NULL;
+					}
+				}
+			}
+			else if (colorspace == fz_device_rgb(ctx))
+			{
+				if (writer->color[0] != color[0] || writer->color[1] != color[1] || writer->color[2] != color[2])
+				{
+					if (color[0] == 0.0f && color[1] == 0.0f && color[2] == 0.0f)
+					{
+						node.cs = CS_RGB_0;
+						color = NULL;
+					}
+					else if (color[0] == 1.0f && color[1] == 1.0f && color[2] == 1.0f)
+					{
+						node.cs = CS_RGB_1;
+						color = NULL;
+					}
+				}
+			}
+			else if (colorspace == fz_device_cmyk(ctx))
+			{
+				if (writer->color[0] != color[0] || writer->color[1] != color[1] || writer->color[2] != color[2] || writer->color[3] != color[3])
+				{
+					if (color[0] == 0.0f && color[1] == 0.0f && color[2] == 0.0f)
+					{
+						if (color[3] == 0.0f)
+						{
+							node.cs = CS_CMYK_0;
+							color = NULL;
+						}
+						else if (color[3] == 1.0f)
+						{
+							node.cs = CS_CMYK_1;
+							color = NULL;
+						}
+					}
+				}
+			}
+			else
+			{
+				int i;
+				int n = fz_colorspace_n(ctx, colorspace);
+				for (i=0; i < n; i++)
+					if (color[i] != 0.0f)
+						break;
+				if (i == n)
+				{
+					node.cs = CS_OTHER_0;
+					pad_size_for_pointer(list, &size);
+					colorspace_off = size;
+					size += SIZE_IN_NODES(sizeof(fz_colorspace *));
+					color = NULL;
+				}
+			}
+		}
+	}
+	if (color)
+	{
+		int i, n;
+		const float *wc = &writer->color[0];
+
+		assert(colorspace != NULL);
+		n = fz_colorspace_n(ctx, colorspace);
+		i = 0;
+		/* Only check colors if the colorspace is unchanged. If the
+		 * colorspace *has* changed and the colors are implicit then
+		 * this will have been caught above. */
+		if (colorspace == writer->colorspace)
+			for (; i < n; i++)
+				if (color[i] != wc[i])
+					break;
+
+		if (i != n)
+		{
+			node.color = 1;
+			color_off = size;
+			size += n * SIZE_IN_NODES(sizeof(float));
+		}
+	}
+	if (alpha && (*alpha != writer->alpha))
+	{
+		if (*alpha >= 1.0f)
+			node.alpha = ALPHA_1;
+		else if (*alpha <= 0.0f)
+			node.alpha = ALPHA_0;
+		else
+		{
+			alpha_off = size;
+			size += SIZE_IN_NODES(sizeof(float));
+			node.alpha = ALPHA_PRESENT;
+		}
+	}
+	if (ctm && (ctm->a != writer->ctm.a || ctm->b != writer->ctm.b || ctm->c != writer->ctm.c || ctm->d != writer->ctm.d || ctm->e != writer->ctm.e || ctm->f != writer->ctm.f))
+	{
+		int ctm_flags;
+
+		ctm_off = size;
+		ctm_flags = CTM_UNCHANGED;
+		if (ctm->a != writer->ctm.a || ctm->d != writer->ctm.d)
+			ctm_flags = CTM_CHANGE_AD, size += SIZE_IN_NODES(2*sizeof(float));
+		if (ctm->b != writer->ctm.b || ctm->c != writer->ctm.c)
+			ctm_flags |= CTM_CHANGE_BC, size += SIZE_IN_NODES(2*sizeof(float));
+		if (ctm->e != writer->ctm.e || ctm->f != writer->ctm.f)
+			ctm_flags |= CTM_CHANGE_EF, size += SIZE_IN_NODES(2*sizeof(float));
+		node.ctm = ctm_flags;
+	}
+	if (stroke && (writer->stroke == NULL || !fz_stroke_state_eq(ctx, stroke, writer->stroke)))
+	{
+		pad_size_for_pointer(list, &size);
+		stroke_off = size;
+		size += SIZE_IN_NODES(sizeof(fz_stroke_state *));
+		node.stroke = 1;
+	}
+	if (path && (writer->path == NULL || path != writer->path))
+	{
+		pad_size_for_pointer(list, &size);
+		path_size = SIZE_IN_NODES(fz_pack_path(ctx, NULL, path));
+		node.path = 1;
+		path_off = size;
+
+		size += path_size;
+	}
+	if (private_data_len)
+	{
+		if (cmd_needs_alignment(cmd))
+			pad_size_for_pointer(list, &size);
+		private_off = size;
+		size += SIZE_IN_NODES(private_data_len);
+	}
+
+	/* If the size is more than 511, then we can't signal that in 9 bits,
+	 * so we'll send it as 511, and then put an extra size_t with the
+	 * size in. */
+	if (size >= INDIRECT_NODE_THRESHOLD)
+		size += SIZE_IN_NODES(sizeof(size_t));
+
+	while (list->len + size > list->max)
+	{
+		size_t newsize = list->max * 2;
+		fz_display_node *old = list->list;
+		ptrdiff_t diff;
+		int i, n;
+
+		if (newsize < 256)
+			newsize = 256;
+		list->list = fz_realloc_array(ctx, list->list, newsize, fz_display_node);
+		list->max = newsize;
+		diff = (char *)(list->list) - (char *)old;
+		n = (writer->top < STACK_SIZE ? writer->top : STACK_SIZE);
+		for (i = 0; i < n; i++)
+		{
+			if (writer->stack[i].update != NULL)
+				writer->stack[i].update = (fz_rect *)(((char *)writer->stack[i].update) + diff);
+		}
+		if (writer->path)
+			writer->path = (fz_path *)(((char *)writer->path) + diff);
+	}
+
+	/* Write the node to the list */
+	if (size >= INDIRECT_NODE_THRESHOLD)
+		node.size = INDIRECT_NODE_THRESHOLD;
+	else
+		node.size = (unsigned int)size;
+	node.flags = flags;
+	node_ptr = &list->list[list->len];
+	*node_ptr = node;
+
+	/* Insert the explicit size (unaligned) if required. */
+	if (size >= INDIRECT_NODE_THRESHOLD)
+	{
+		memcpy(&node_ptr[1], &size, sizeof(size));
+		node_ptr += SIZE_IN_NODES(sizeof(size_t));
+	}
+
+	/* Path is the most frequent one, so try to avoid the try/catch in
+	 * this case */
+	if (path_off)
+	{
+		my_path = (void *)(&node_ptr[path_off]);
+		(void)fz_pack_path(ctx, (void *)my_path, path);
+	}
+
+	if (stroke_off)
+	{
+		fz_try(ctx)
+		{
+			my_stroke = fz_clone_stroke_state(ctx, stroke);
+		}
+		fz_catch(ctx)
+		{
+			fz_drop_path(ctx, my_path);
+			fz_rethrow(ctx);
+		}
+	}
+
+	if (rect_off)
+	{
+		fz_rect *out_rect = (fz_rect *)(void *)(&node_ptr[rect_off]);
+		writer->rect = *rect;
+		*out_rect = *rect;
+		if (rect_for_updates)
+			writer->stack[writer->top-1].update = out_rect;
+	}
+	if (path_off)
+	{
+		fz_drop_path(ctx, writer->path);
+		writer->path = fz_keep_path(ctx, my_path); /* Can never fail */
+	}
+	if (node.cs)
+	{
+		fz_drop_colorspace(ctx, writer->colorspace);
+		switch(node.cs)
+		{
+		case CS_GRAY_0:
+			writer->colorspace = fz_keep_colorspace(ctx, fz_device_gray(ctx));
+			writer->color[0] = 0;
+			break;
+		case CS_GRAY_1:
+			writer->colorspace = fz_keep_colorspace(ctx, fz_device_gray(ctx));
+			writer->color[0] = 1;
+			break;
+		case CS_RGB_0:
+			writer->color[0] = 0;
+			writer->color[1] = 0;
+			writer->color[2] = 0;
+			writer->colorspace = fz_keep_colorspace(ctx, fz_device_rgb(ctx));
+			break;
+		case CS_RGB_1:
+			writer->color[0] = 1;
+			writer->color[1] = 1;
+			writer->color[2] = 1;
+			writer->colorspace = fz_keep_colorspace(ctx, fz_device_rgb(ctx));
+			break;
+		case CS_CMYK_0:
+			writer->color[0] = 0;
+			writer->color[1] = 0;
+			writer->color[2] = 0;
+			writer->color[3] = 0;
+			writer->colorspace = fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
+			break;
+		case CS_CMYK_1:
+			writer->color[0] = 0;
+			writer->color[1] = 0;
+			writer->color[2] = 0;
+			writer->color[3] = 1;
+			writer->colorspace = fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
+			break;
+		default:
+		{
+			fz_colorspace **out_colorspace = (fz_colorspace **)(void *)(&node_ptr[colorspace_off]);
+			int i, n;
+			n = fz_colorspace_n(ctx, colorspace);
+			*out_colorspace = fz_keep_colorspace(ctx, colorspace);
+
+			writer->colorspace = fz_keep_colorspace(ctx, colorspace);
+			for (i = 0; i < n; i++)
+				writer->color[i] = 0;
+			break;
+		}
+		}
+	}
+	if (color_off)
+	{
+		int n = fz_colorspace_n(ctx, colorspace);
+		float *out_color = (float *)(void *)(&node_ptr[color_off]);
+		memcpy(writer->color, color, n * sizeof(float));
+		memcpy(out_color, color, n * sizeof(float));
+	}
+	if (node.alpha)
+	{
+		writer->alpha = *alpha;
+		if (alpha_off)
+		{
+			float *out_alpha = (float *)(void *)(&node_ptr[alpha_off]);
+			*out_alpha = *alpha;
+		}
+	}
+	if (ctm_off)
+	{
+		float *out_ctm = (float *)(void *)(&node_ptr[ctm_off]);
+		if (node.ctm & CTM_CHANGE_AD)
+		{
+			writer->ctm.a = *out_ctm++ = ctm->a;
+			writer->ctm.d = *out_ctm++ = ctm->d;
+		}
+		if (node.ctm & CTM_CHANGE_BC)
+		{
+			writer->ctm.b = *out_ctm++ = ctm->b;
+			writer->ctm.c = *out_ctm++ = ctm->c;
+		}
+		if (node.ctm & CTM_CHANGE_EF)
+		{
+			writer->ctm.e = *out_ctm++ = ctm->e;
+			writer->ctm.f = *out_ctm = ctm->f;
+		}
+	}
+	if (stroke_off)
+	{
+		fz_stroke_state **out_stroke = (fz_stroke_state **)(void *)(&node_ptr[stroke_off]);
+		*out_stroke = my_stroke;
+		fz_drop_stroke_state(ctx, writer->stroke);
+		/* Can never fail as my_stroke was cloned above */
+		writer->stroke = fz_keep_stroke_state(ctx, my_stroke);
+	}
+	if (private_data_len)
+	{
+		out_private = (unsigned char *)(void *)(&node_ptr[private_off]);
+		if (private_data)
+			memcpy(out_private, private_data, private_data_len);
+	}
+	list->len += size;
+
+	return out_private;
+}
+
+/* Pack ri, op, opm, bp into flags upper bits, even/odd in lower bit */
+static int
+fz_pack_color_params(fz_color_params color_params)
+{
+	int flags = 0;
+	flags |= color_params.ri << RI; /* 2 bits */
+	flags |= color_params.bp << BP;
+	flags |= color_params.op << OP;
+	flags |= color_params.opm << OPM;
+	return flags;
+}
+
+/* unpack ri, op, opm, bp from flags, even/odd in lower bit */
+static void
+fz_unpack_color_params(fz_color_params *color_params, int flags)
+{
+	color_params->ri = (flags >> RI) & 3;
+	color_params->bp = (flags >> BP) & 1;
+	color_params->op = (flags >> OP) & 1;
+	color_params->opm = (flags >> OPM) & 1;
+}
+
+static void
+fz_list_fill_path(fz_context *ctx, fz_device *dev, const fz_path *path, int even_odd, fz_matrix ctm,
+	fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
+{
+	fz_rect rect = fz_bound_path(ctx, path, NULL, ctm);
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_FILL_PATH,
+		even_odd | fz_pack_color_params(color_params), /* flags */
+		&rect,
+		path, /* path */
+		color,
+		colorspace,
+		&alpha, /* alpha */
+		&ctm,
+		NULL, /* stroke_state */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_stroke_path(fz_context *ctx, fz_device *dev, const fz_path *path, const fz_stroke_state *stroke,
+	fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
+{
+	fz_rect rect = fz_bound_path(ctx, path, stroke, ctm);
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_STROKE_PATH,
+		fz_pack_color_params(color_params), /* flags */
+		&rect,
+		path, /* path */
+		color,
+		colorspace,
+		&alpha, /* alpha */
+		&ctm, /* ctm */
+		stroke,
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_clip_path(fz_context *ctx, fz_device *dev, const fz_path *path, int even_odd, fz_matrix ctm, fz_rect scissor)
+{
+	fz_rect rect = fz_bound_path(ctx, path, NULL, ctm);
+	rect = fz_intersect_rect(rect, scissor);
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_CLIP_PATH,
+		even_odd, /* flags */
+		&rect,
+		path, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		&ctm, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_clip_stroke_path(fz_context *ctx, fz_device *dev, const fz_path *path, const fz_stroke_state *stroke, fz_matrix ctm, fz_rect scissor)
+{
+	fz_rect rect = fz_bound_path(ctx, path, stroke, ctm);
+	rect = fz_intersect_rect(rect, scissor);
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_CLIP_STROKE_PATH,
+		0, /* flags */
+		&rect,
+		path, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		&ctm, /* ctm */
+		stroke, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_fill_text(fz_context *ctx, fz_device *dev, const fz_text *text, fz_matrix ctm,
+	fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
+{
+	fz_text *cloned_text = fz_keep_text(ctx, text);
+	fz_try(ctx)
+	{
+		fz_rect rect = fz_bound_text(ctx, text, NULL, ctm);
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_FILL_TEXT,
+			fz_pack_color_params(color_params), /* flags */
+			&rect,
+			NULL, /* path */
+			color, /* color */
+			colorspace, /* colorspace */
+			&alpha, /* alpha */
+			&ctm, /* ctm */
+			NULL, /* stroke */
+			&cloned_text, /* private_data */
+			sizeof(cloned_text)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_text(ctx, cloned_text);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_stroke_text(fz_context *ctx, fz_device *dev, const fz_text *text, const fz_stroke_state *stroke, fz_matrix ctm,
+	fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
+{
+	fz_text *cloned_text = fz_keep_text(ctx, text);
+	fz_try(ctx)
+	{
+		fz_rect rect = fz_bound_text(ctx, text, stroke, ctm);
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_STROKE_TEXT,
+			fz_pack_color_params(color_params), /* flags */
+			&rect,
+			NULL, /* path */
+			color, /* color */
+			colorspace, /* colorspace */
+			&alpha, /* alpha */
+			&ctm, /* ctm */
+			stroke,
+			&cloned_text, /* private_data */
+			sizeof(cloned_text)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_text(ctx, cloned_text);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_clip_text(fz_context *ctx, fz_device *dev, const fz_text *text, fz_matrix ctm, fz_rect scissor)
+{
+	fz_text *cloned_text = fz_keep_text(ctx, text);
+	fz_try(ctx)
+	{
+		fz_rect rect = fz_bound_text(ctx, text, NULL, ctm);
+		rect = fz_intersect_rect(rect, scissor);
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_CLIP_TEXT,
+			0, /* flags */
+			&rect,
+			NULL, /* path */
+			NULL, /* color */
+			NULL, /* colorspace */
+			NULL, /* alpha */
+			&ctm, /* ctm */
+			NULL, /* stroke */
+			&cloned_text, /* private_data */
+			sizeof(cloned_text)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_text(ctx, cloned_text);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_clip_stroke_text(fz_context *ctx, fz_device *dev, const fz_text *text, const fz_stroke_state *stroke, fz_matrix ctm, fz_rect scissor)
+{
+	fz_text *cloned_text = fz_keep_text(ctx, text);
+	fz_try(ctx)
+	{
+		fz_rect rect = fz_bound_text(ctx, text, stroke, ctm);
+		rect = fz_intersect_rect(rect, scissor);
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_CLIP_STROKE_TEXT,
+			0, /* flags */
+			&rect,
+			NULL, /* path */
+			NULL, /* color */
+			NULL, /* colorspace */
+			NULL, /* alpha */
+			&ctm, /* ctm */
+			stroke, /* stroke */
+			&cloned_text, /* private_data */
+			sizeof(cloned_text)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_text(ctx, cloned_text);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_ignore_text(fz_context *ctx, fz_device *dev, const fz_text *text, fz_matrix ctm)
+{
+	fz_text *cloned_text = fz_keep_text(ctx, text);
+	fz_try(ctx)
+	{
+		fz_rect rect = fz_bound_text(ctx, text, NULL, ctm);
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_IGNORE_TEXT,
+			0, /* flags */
+			&rect,
+			NULL, /* path */
+			NULL, /* color */
+			NULL, /* colorspace */
+			NULL, /* alpha */
+			&ctm, /* ctm */
+			NULL, /* stroke */
+			&cloned_text, /* private_data */
+			sizeof(cloned_text)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_text(ctx, cloned_text);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_pop_clip(fz_context *ctx, fz_device *dev)
+{
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_POP_CLIP,
+		0, /* flags */
+		NULL, /* rect */
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_fill_shade(fz_context *ctx, fz_device *dev, fz_shade *shade, fz_matrix ctm, float alpha, fz_color_params color_params)
+{
+	fz_shade *shade2 = fz_keep_shade(ctx, shade);
+	fz_try(ctx)
+	{
+		fz_rect rect = fz_bound_shade(ctx, shade, ctm);
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_FILL_SHADE,
+			fz_pack_color_params(color_params), /* flags */
+			&rect,
+			NULL, /* path */
+			NULL, /* color */
+			NULL, /* colorspace */
+			&alpha, /* alpha */
+			&ctm, /* ctm */
+			NULL, /* stroke */
+			&shade2, /* private_data */
+			sizeof(shade2)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_shade(ctx, shade2);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_fill_image(fz_context *ctx, fz_device *dev, fz_image *image, fz_matrix ctm, float alpha, fz_color_params color_params)
+{
+	fz_image *image2 = fz_keep_image(ctx, image);
+	fz_try(ctx)
+	{
+		fz_rect rect = fz_transform_rect(fz_unit_rect, ctm);
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_FILL_IMAGE,
+			fz_pack_color_params(color_params), /* flags */
+			&rect,
+			NULL, /* path */
+			NULL, /* color */
+			NULL, /* colorspace */
+			&alpha, /* alpha */
+			&ctm, /* ctm */
+			NULL, /* stroke */
+			&image2, /* private_data */
+			sizeof(image2)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_image(ctx, image2);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_fill_image_mask(fz_context *ctx, fz_device *dev, fz_image *image, fz_matrix ctm,
+	fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
+{
+	fz_image *image2 = fz_keep_image(ctx, image);
+
+	fz_try(ctx)
+	{
+		fz_rect rect = fz_transform_rect(fz_unit_rect, ctm);
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_FILL_IMAGE_MASK,
+			fz_pack_color_params(color_params), /* flags */
+			&rect,
+			NULL, /* path */
+			color,
+			colorspace,
+			&alpha, /* alpha */
+			&ctm, /* ctm */
+			NULL, /* stroke */
+			&image2, /* private_data */
+			sizeof(image2)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_image(ctx, image2);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_clip_image_mask(fz_context *ctx, fz_device *dev, fz_image *image, fz_matrix ctm, fz_rect scissor)
+{
+	fz_image *image2 = fz_keep_image(ctx, image);
+	fz_try(ctx)
+	{
+		fz_rect rect = fz_transform_rect(fz_unit_rect, ctm);
+		rect = fz_intersect_rect(rect, scissor);
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_CLIP_IMAGE_MASK,
+			0, /* flags */
+			&rect,
+			NULL, /* path */
+			NULL, /* color */
+			NULL, /* colorspace */
+			NULL, /* alpha */
+			&ctm, /* ctm */
+			NULL, /* stroke */
+			&image2, /* private_data */
+			sizeof(image2)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_image(ctx, image2);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_begin_mask(fz_context *ctx, fz_device *dev, fz_rect rect, int luminosity, fz_colorspace *colorspace, const float *color, fz_color_params color_params)
+{
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_BEGIN_MASK,
+		(!!luminosity) | fz_pack_color_params(color_params), /* flags */
+		&rect,
+		NULL, /* path */
+		color,
+		colorspace,
+		NULL, /* alpha */
+		NULL, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_end_mask(fz_context *ctx, fz_device *dev, fz_function *tr)
+{
+	fz_function *tr2 = fz_keep_function(ctx, tr);
+
+	fz_try(ctx)
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_END_MASK,
+			0, /* flags */
+			NULL, /* rect */
+			NULL, /* path */
+			NULL, /* color */
+			NULL, /* colorspace */
+			NULL, /* alpha */
+			NULL, /* ctm */
+			NULL, /* stroke */
+			&tr2, /* private_data */
+			sizeof(tr2)); /* private_data_len */
+	fz_catch(ctx)
+	{
+		fz_drop_function(ctx, tr);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_begin_group(fz_context *ctx, fz_device *dev, fz_rect rect, fz_colorspace *colorspace, int isolated, int knockout, int blendmode, float alpha)
+{
+	int flags;
+	static const float color[FZ_MAX_COLORS] = { 0 };
+
+	flags = (blendmode<<2);
+	if (isolated)
+		flags |= ISOLATED;
+	if (knockout)
+		flags |= KNOCKOUT;
+
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_BEGIN_GROUP,
+		flags,
+		&rect,
+		NULL, /* path */
+		color, /* color */
+		colorspace, /* colorspace */
+		&alpha, /* alpha */
+		NULL, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_end_group(fz_context *ctx, fz_device *dev)
+{
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_END_GROUP,
+		0, /* flags */
+		NULL, /* rect */
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+typedef struct
+{
+	float xstep;
+	float ystep;
+	fz_rect view;
+	int id;
+	int doc_id;
+} fz_list_tile_data;
+
+static int
+fz_list_begin_tile(fz_context *ctx, fz_device *dev, fz_rect area, fz_rect view, float xstep, float ystep, fz_matrix ctm, int id, int doc_id)
+{
+	fz_list_tile_data tile;
+
+	tile.xstep = xstep;
+	tile.ystep = ystep;
+	tile.view = view;
+	tile.id = id;
+	tile.doc_id = doc_id;
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_BEGIN_TILE,
+		0, /* flags */
+		&area,
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		&ctm, /* ctm */
+		NULL, /* stroke */
+		&tile, /* private_data */
+		sizeof(tile)); /* private_data_len */
+
+	return 0;
+}
+
+static void
+fz_list_end_tile(fz_context *ctx, fz_device *dev)
+{
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_END_TILE,
+		0, /* flags */
+		NULL,
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_render_flags(fz_context *ctx, fz_device *dev, int set, int clear)
+{
+	int flags;
+
+	/* Pack the options down */
+	if (set == FZ_DEVFLAG_GRIDFIT_AS_TILED && clear == 0)
+		flags = 1;
+	else if (set == 0 && clear == FZ_DEVFLAG_GRIDFIT_AS_TILED)
+		flags = 0;
+	else
+	{
+		assert("Unsupported flags combination" == NULL);
+		return;
+	}
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_RENDER_FLAGS,
+		flags, /* flags */
+		NULL,
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_set_default_colorspaces(fz_context *ctx, fz_device *dev, fz_default_colorspaces *default_cs)
+{
+	fz_default_colorspaces *default_cs2 = fz_keep_default_colorspaces(ctx, default_cs);
+
+	fz_try(ctx)
+	{
+		fz_append_display_node(
+			ctx,
+			dev,
+			FZ_CMD_DEFAULT_COLORSPACES,
+			0, /* flags */
+			NULL,
+			NULL, /* path */
+			NULL, /* color */
+			NULL, /* colorspace */
+			NULL, /* alpha */
+			NULL, /* ctm */
+			NULL, /* stroke */
+			&default_cs2, /* private_data */
+			sizeof(default_cs2)); /* private_data_len */
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_default_colorspaces(ctx, default_cs2);
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+fz_list_begin_layer(fz_context *ctx, fz_device *dev, const char *layer_name)
+{
+	size_t len = layer_name ? strlen(layer_name) : 0;
+
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_BEGIN_LAYER,
+		0, /* flags */
+		NULL,
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL,
+		NULL, /* stroke */
+		len ? layer_name : "", /* private_data */
+		len + 1); /* private_data_len */
+}
+
+static void
+fz_list_end_layer(fz_context *ctx, fz_device *dev)
+{
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_END_LAYER,
+		0, /* flags */
+		NULL,
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_begin_structure(fz_context *ctx, fz_device *dev, fz_structure standard, const char *raw, int idx)
+{
+	unsigned char *data;
+	size_t len = (raw ? strlen(raw) : 0);
+
+	data = fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_BEGIN_STRUCTURE,
+		0, /* flags */
+		NULL,
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL,
+		NULL, /* stroke */
+		NULL, /* private_data */
+		len+2+sizeof(idx)); /* private_data_len */
+	data[0] = (char)standard;
+	memcpy(data+1, &idx, sizeof(idx));
+	if (len)
+		memcpy(data+1+sizeof(idx), raw, len+1);
+	else
+		data[1+sizeof(idx)] = 0;
+}
+
+static void
+fz_list_end_structure(fz_context *ctx, fz_device *dev)
+{
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_END_STRUCTURE,
+		0, /* flags */
+		NULL,
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_begin_metatext(fz_context *ctx, fz_device *dev, fz_metatext meta, const char *text)
+{
+	unsigned char *data;
+	size_t len = (text ? strlen(text) : 0);
+
+	data = fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_BEGIN_METATEXT,
+		0, /* flags */
+		NULL,
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL,
+		NULL, /* stroke */
+		NULL, /* private_data */
+		len + 2); /* private_data_len */
+	data[0] = (char)meta;
+	if (len)
+		memcpy(data+1, text, len+1);
+	else
+		data[1] = 0;
+}
+
+static void
+fz_list_end_metatext(fz_context *ctx, fz_device *dev)
+{
+	fz_append_display_node(
+		ctx,
+		dev,
+		FZ_CMD_END_METATEXT,
+		0, /* flags */
+		NULL,
+		NULL, /* path */
+		NULL, /* color */
+		NULL, /* colorspace */
+		NULL, /* alpha */
+		NULL, /* ctm */
+		NULL, /* stroke */
+		NULL, /* private_data */
+		0); /* private_data_len */
+}
+
+static void
+fz_list_drop_device(fz_context *ctx, fz_device *dev)
+{
+	fz_list_device *writer = (fz_list_device *)dev;
+
+	fz_drop_colorspace(ctx, writer->colorspace);
+	fz_drop_stroke_state(ctx, writer->stroke);
+	fz_drop_path(ctx, writer->path);
+	fz_drop_display_list(ctx, writer->list);
+}
+
+fz_device *
+fz_new_list_device(fz_context *ctx, fz_display_list *list)
+{
+	fz_list_device *dev;
+
+	dev = fz_new_derived_device(ctx, fz_list_device);
+
+	dev->super.fill_path = fz_list_fill_path;
+	dev->super.stroke_path = fz_list_stroke_path;
+	dev->super.clip_path = fz_list_clip_path;
+	dev->super.clip_stroke_path = fz_list_clip_stroke_path;
+
+	dev->super.fill_text = fz_list_fill_text;
+	dev->super.stroke_text = fz_list_stroke_text;
+	dev->super.clip_text = fz_list_clip_text;
+	dev->super.clip_stroke_text = fz_list_clip_stroke_text;
+	dev->super.ignore_text = fz_list_ignore_text;
+
+	dev->super.fill_shade = fz_list_fill_shade;
+	dev->super.fill_image = fz_list_fill_image;
+	dev->super.fill_image_mask = fz_list_fill_image_mask;
+	dev->super.clip_image_mask = fz_list_clip_image_mask;
+
+	dev->super.pop_clip = fz_list_pop_clip;
+
+	dev->super.begin_mask = fz_list_begin_mask;
+	dev->super.end_mask = fz_list_end_mask;
+	dev->super.begin_group = fz_list_begin_group;
+	dev->super.end_group = fz_list_end_group;
+
+	dev->super.begin_tile = fz_list_begin_tile;
+	dev->super.end_tile = fz_list_end_tile;
+
+	dev->super.render_flags = fz_list_render_flags;
+	dev->super.set_default_colorspaces = fz_list_set_default_colorspaces;
+
+	dev->super.begin_layer = fz_list_begin_layer;
+	dev->super.end_layer = fz_list_end_layer;
+
+	dev->super.begin_structure = fz_list_begin_structure;
+	dev->super.end_structure = fz_list_end_structure;
+
+	dev->super.begin_metatext = fz_list_begin_metatext;
+	dev->super.end_metatext = fz_list_end_metatext;
+
+	dev->super.drop_device = fz_list_drop_device;
+
+	dev->list = fz_keep_display_list(ctx, list);
+	dev->path = NULL;
+	dev->alpha = 1.0f;
+	dev->ctm = fz_identity;
+	dev->stroke = NULL;
+	dev->colorspace = fz_keep_colorspace(ctx, fz_device_gray(ctx));
+	memset(dev->color, 0, sizeof(float)*FZ_MAX_COLORS);
+	dev->top = 0;
+	dev->tiled = 0;
+
+	return &dev->super;
+}
+
+static void
+fz_drop_display_list_imp(fz_context *ctx, fz_storable *list_)
+{
+	fz_display_list *list = (fz_display_list *)list_;
+	fz_display_node *node = list->list;
+	fz_display_node *node_end = list->list + list->len;
+	int cs_n = 1;
+	fz_colorspace *cs;
+
+	while (node != node_end)
+	{
+		fz_display_node n = *node;
+		size_t size = n.size;
+		fz_display_node *next;
+
+		if (size == INDIRECT_NODE_THRESHOLD)
+		{
+			memcpy(&size, &node[1], sizeof(size));
+			node += SIZE_IN_NODES(sizeof(size_t));
+			size -= SIZE_IN_NODES(sizeof(size_t));
+		}
+
+		next = node + size;
+
+		node++;
+		if (n.rect)
+		{
+			node += SIZE_IN_NODES(sizeof(fz_rect));
+		}
+		switch (n.cs)
+		{
+		default:
+		case CS_UNCHANGED:
+			break;
+		case CS_GRAY_0:
+		case CS_GRAY_1:
+			cs_n = 1;
+			break;
+		case CS_RGB_0:
+		case CS_RGB_1:
+			cs_n = 3;
+			break;
+		case CS_CMYK_0:
+		case CS_CMYK_1:
+			cs_n = 4;
+			break;
+		case CS_OTHER_0:
+			align_node_for_pointer(&node);
+			cs = *(fz_colorspace **)node;
+			cs_n = fz_colorspace_n(ctx, cs);
+			fz_drop_colorspace(ctx, cs);
+			node += SIZE_IN_NODES(sizeof(fz_colorspace *));
+			break;
+		}
+		if (n.color)
+		{
+			node += SIZE_IN_NODES(cs_n * sizeof(float));
+		}
+		if (n.alpha == ALPHA_PRESENT)
+		{
+			node += SIZE_IN_NODES(sizeof(float));
+		}
+		if (n.ctm & CTM_CHANGE_AD)
+			node += SIZE_IN_NODES(2*sizeof(float));
+		if (n.ctm & CTM_CHANGE_BC)
+			node += SIZE_IN_NODES(2*sizeof(float));
+		if (n.ctm & CTM_CHANGE_EF)
+			node += SIZE_IN_NODES(2*sizeof(float));
+		if (n.stroke)
+		{
+			align_node_for_pointer(&node);
+			fz_drop_stroke_state(ctx, *(fz_stroke_state **)node);
+			node += SIZE_IN_NODES(sizeof(fz_stroke_state *));
+		}
+		if (n.path)
+		{
+			int path_size;
+			align_node_for_pointer(&node);
+			path_size = fz_packed_path_size((fz_path *)node);
+			fz_drop_path(ctx, (fz_path *)node);
+			node += SIZE_IN_NODES(path_size);
+		}
+		switch(n.cmd)
+		{
+		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:
+			align_node_for_pointer(&node);
+			fz_drop_text(ctx, *(fz_text **)node);
+			break;
+		case FZ_CMD_FILL_SHADE:
+			align_node_for_pointer(&node);
+			fz_drop_shade(ctx, *(fz_shade **)node);
+			break;
+		case FZ_CMD_FILL_IMAGE:
+		case FZ_CMD_FILL_IMAGE_MASK:
+		case FZ_CMD_CLIP_IMAGE_MASK:
+			align_node_for_pointer(&node);
+			fz_drop_image(ctx, *(fz_image **)node);
+			break;
+		case FZ_CMD_END_MASK:
+			align_node_for_pointer(&node);
+			fz_drop_function(ctx, *(fz_function **)node);
+			break;
+		case FZ_CMD_DEFAULT_COLORSPACES:
+			align_node_for_pointer(&node);
+			fz_drop_default_colorspaces(ctx, *(fz_default_colorspaces **)node);
+			break;
+		}
+		node = next;
+	}
+	fz_free(ctx, list->list);
+	fz_free(ctx, list);
+}
+
+fz_display_list *
+fz_new_display_list(fz_context *ctx, fz_rect mediabox)
+{
+	fz_display_list *list = fz_malloc_struct(ctx, fz_display_list);
+	FZ_INIT_STORABLE(list, 1, fz_drop_display_list_imp);
+	list->list = NULL;
+	list->mediabox = mediabox;
+	list->max = 0;
+	list->len = 0;
+	return list;
+}
+
+fz_display_list *
+fz_keep_display_list(fz_context *ctx, fz_display_list *list)
+{
+	return fz_keep_storable(ctx, &list->storable);
+}
+
+void
+fz_drop_display_list(fz_context *ctx, fz_display_list *list)
+{
+	fz_defer_reap_start(ctx);
+	fz_drop_storable(ctx, &list->storable);
+	fz_defer_reap_end(ctx);
+}
+
+fz_rect
+fz_bound_display_list(fz_context *ctx, fz_display_list *list)
+{
+	return list->mediabox;
+}
+
+int fz_display_list_is_empty(fz_context *ctx, const fz_display_list *list)
+{
+	return !list || list->len == 0;
+}
+
+void
+fz_run_display_list(fz_context *ctx, fz_display_list *list, fz_device *dev, fz_matrix top_ctm, fz_rect scissor, fz_cookie *cookie)
+{
+	fz_display_node *node;
+	fz_display_node *node_end;
+	fz_display_node *next_node;
+	int clipped = 0;
+	int tiled = 0;
+	int progress = 0;
+
+	/* Current graphics state as unpacked from list */
+	fz_path *path = NULL;
+	float alpha = 1.0f;
+	fz_matrix ctm = fz_identity;
+	fz_stroke_state *stroke = NULL;
+	float color[FZ_MAX_COLORS] = { 0 };
+	fz_colorspace *colorspace = fz_keep_colorspace(ctx, fz_device_gray(ctx));
+	fz_color_params color_params;
+	fz_rect rect = { 0 };
+
+	/* Transformed versions of graphic state entries */
+	fz_rect trans_rect;
+	fz_matrix trans_ctm;
+	int tile_skip_depth = 0;
+
+	if (cookie)
+	{
+		cookie->progress_max = list->len;
+		cookie->progress = 0;
+	}
+
+	color_params = fz_default_color_params;
+
+	node = list->list;
+	node_end = &list->list[list->len];
+	for (; node != node_end ; node = next_node)
+	{
+		int empty;
+		fz_display_node n = *node;
+		size_t size = n.size;
+
+		if (size == INDIRECT_NODE_THRESHOLD)
+		{
+			memcpy(&size, &node[1], sizeof(size_t));
+			node += SIZE_IN_NODES(sizeof(size_t));
+			size -= SIZE_IN_NODES(sizeof(size_t));
+		}
+
+		next_node = node + size;
+
+		/* Check the cookie for aborting */
+		if (cookie)
+		{
+			if (cookie->abort)
+				break;
+			cookie->progress = progress;
+			progress += (int)size;
+		}
+
+		node++;
+		if (n.rect)
+		{
+			rect = *(fz_rect *)node;
+			node += SIZE_IN_NODES(sizeof(fz_rect));
+		}
+		if (n.cs)
+		{
+			int i, en;
+
+			fz_drop_colorspace(ctx, colorspace);
+			switch (n.cs)
+			{
+			default:
+			case CS_GRAY_0:
+				colorspace = fz_keep_colorspace(ctx, fz_device_gray(ctx));
+				color[0] = 0.0f;
+				break;
+			case CS_GRAY_1:
+				colorspace = fz_keep_colorspace(ctx, fz_device_gray(ctx));
+				color[0] = 1.0f;
+				break;
+			case CS_RGB_0:
+				colorspace = fz_keep_colorspace(ctx, fz_device_rgb(ctx));
+				color[0] = 0.0f;
+				color[1] = 0.0f;
+				color[2] = 0.0f;
+				break;
+			case CS_RGB_1:
+				colorspace = fz_keep_colorspace(ctx, fz_device_rgb(ctx));
+				color[0] = 1.0f;
+				color[1] = 1.0f;
+				color[2] = 1.0f;
+				break;
+			case CS_CMYK_0:
+				colorspace = fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
+				color[0] = 0.0f;
+				color[1] = 0.0f;
+				color[2] = 0.0f;
+				color[3] = 0.0f;
+				break;
+			case CS_CMYK_1:
+				colorspace = fz_keep_colorspace(ctx, fz_device_cmyk(ctx));
+				color[0] = 0.0f;
+				color[1] = 0.0f;
+				color[2] = 0.0f;
+				color[3] = 1.0f;
+				break;
+			case CS_OTHER_0:
+				align_node_for_pointer(&node);
+				colorspace = fz_keep_colorspace(ctx, *(fz_colorspace **)(node));
+				node += SIZE_IN_NODES(sizeof(fz_colorspace *));
+				en = fz_colorspace_n(ctx, colorspace);
+				for (i = 0; i < en; i++)
+					color[i] = 0.0f;
+				break;
+			}
+		}
+		if (n.color)
+		{
+			int nc = fz_colorspace_n(ctx, colorspace);
+			memcpy(color, (float *)node, nc * sizeof(float));
+			node += SIZE_IN_NODES(nc * sizeof(float));
+		}
+		if (n.alpha)
+		{
+			switch(n.alpha)
+			{
+			default:
+			case ALPHA_0:
+				alpha = 0.0f;
+				break;
+			case ALPHA_1:
+				alpha = 1.0f;
+				break;
+			case ALPHA_PRESENT:
+				alpha = *(float *)node;
+				node += SIZE_IN_NODES(sizeof(float));
+				break;
+			}
+		}
+		if (n.ctm != 0)
+		{
+			float *packed_ctm = (float *)node;
+			if (n.ctm & CTM_CHANGE_AD)
+			{
+				ctm.a = *packed_ctm++;
+				ctm.d = *packed_ctm++;
+				node += SIZE_IN_NODES(2*sizeof(float));
+			}
+			if (n.ctm & CTM_CHANGE_BC)
+			{
+				ctm.b = *packed_ctm++;
+				ctm.c = *packed_ctm++;
+				node += SIZE_IN_NODES(2*sizeof(float));
+			}
+			if (n.ctm & CTM_CHANGE_EF)
+			{
+				ctm.e = *packed_ctm++;
+				ctm.f = *packed_ctm;
+				node += SIZE_IN_NODES(2*sizeof(float));
+			}
+		}
+		if (n.stroke)
+		{
+			align_node_for_pointer(&node);
+			fz_drop_stroke_state(ctx, stroke);
+			stroke = fz_keep_stroke_state(ctx, *(fz_stroke_state **)node);
+			node += SIZE_IN_NODES(sizeof(fz_stroke_state *));
+		}
+		if (n.path)
+		{
+			align_node_for_pointer(&node);
+			fz_drop_path(ctx, path);
+			path = fz_keep_path(ctx, (fz_path *)node);
+			node += SIZE_IN_NODES(fz_packed_path_size(path));
+		}
+
+		if (tile_skip_depth > 0)
+		{
+			if (n.cmd == FZ_CMD_BEGIN_TILE)
+				tile_skip_depth++;
+			else if (n.cmd == FZ_CMD_END_TILE)
+				tile_skip_depth--;
+			if (tile_skip_depth > 0)
+				continue;
+		}
+
+		trans_rect = fz_transform_rect(rect, top_ctm);
+
+		/* cull objects to draw using a quick visibility test */
+
+		if (tiled ||
+			n.cmd == FZ_CMD_BEGIN_TILE || n.cmd == FZ_CMD_END_TILE ||
+			n.cmd == FZ_CMD_RENDER_FLAGS || n.cmd == FZ_CMD_DEFAULT_COLORSPACES ||
+			n.cmd == FZ_CMD_BEGIN_LAYER || n.cmd == FZ_CMD_END_LAYER ||
+			n.cmd == FZ_CMD_BEGIN_STRUCTURE || n.cmd == FZ_CMD_END_STRUCTURE ||
+			n.cmd == FZ_CMD_BEGIN_METATEXT || n.cmd == FZ_CMD_END_METATEXT
+			)
+		{
+			empty = 0;
+		}
+		else if (n.cmd == FZ_CMD_FILL_PATH || n.cmd == FZ_CMD_STROKE_PATH)
+		{
+			/* Zero area paths are suitable for stroking. */
+			empty = !fz_is_valid_rect(fz_intersect_rect(trans_rect, scissor));
+		}
+		else if (n.cmd == FZ_CMD_FILL_TEXT || n.cmd == FZ_CMD_STROKE_TEXT ||
+			n.cmd == FZ_CMD_CLIP_TEXT || n.cmd == FZ_CMD_CLIP_STROKE_TEXT)
+		{
+			/* Zero area text (such as spaces) should be passed
+			 * through. Text that is completely outside the scissor
+			 * can be elided. */
+			empty = !fz_is_valid_rect(fz_intersect_rect(trans_rect, scissor));
+		}
+		else
+		{
+			empty = fz_is_empty_rect(fz_intersect_rect(trans_rect, scissor));
+		}
+
+		/* clipped starts out as 0. It only goes non-zero here if we move inside
+		 * an 'empty' region. Whenever clipped is non zero, or we are in an empty
+		 * region, we therefore may need to increment clipped according to the
+		 * nesting. */
+		if (clipped || empty)
+		{
+			switch (n.cmd)
+			{
+			case FZ_CMD_CLIP_PATH:
+			case FZ_CMD_CLIP_STROKE_PATH:
+			case FZ_CMD_CLIP_TEXT:
+			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_BEGIN_STRUCTURE:
+			case FZ_CMD_END_STRUCTURE:
+			case FZ_CMD_BEGIN_METATEXT:
+			case FZ_CMD_END_METATEXT:
+				/* These may not nest as nicely as we'd like. Just ignore them for
+				 * the purposes of clipping. */
+				break;
+			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:
+		trans_ctm = fz_concat(ctm, top_ctm);
+
+		fz_try(ctx)
+		{
+			switch (n.cmd)
+			{
+			case FZ_CMD_FILL_PATH:
+				fz_unpack_color_params(&color_params, n.flags);
+				fz_fill_path(ctx, dev, path, n.flags & 1, trans_ctm, colorspace, color, alpha, color_params);
+				break;
+			case FZ_CMD_STROKE_PATH:
+				fz_unpack_color_params(&color_params, n.flags);
+				fz_stroke_path(ctx, dev, path, stroke, trans_ctm, colorspace, color, alpha, color_params);
+				break;
+			case FZ_CMD_CLIP_PATH:
+				fz_clip_path(ctx, dev, path, n.flags, trans_ctm, trans_rect);
+				break;
+			case FZ_CMD_CLIP_STROKE_PATH:
+				fz_clip_stroke_path(ctx, dev, path, stroke, trans_ctm, trans_rect);
+				break;
+			case FZ_CMD_FILL_TEXT:
+				fz_unpack_color_params(&color_params, n.flags);
+				align_node_for_pointer(&node);
+				fz_fill_text(ctx, dev, *(fz_text **)node, trans_ctm, colorspace, color, alpha, color_params);
+				break;
+			case FZ_CMD_STROKE_TEXT:
+				fz_unpack_color_params(&color_params, n.flags);
+				align_node_for_pointer(&node);
+				fz_stroke_text(ctx, dev, *(fz_text **)node, stroke, trans_ctm, colorspace, color, alpha, color_params);
+				break;
+			case FZ_CMD_CLIP_TEXT:
+				align_node_for_pointer(&node);
+				fz_clip_text(ctx, dev, *(fz_text **)node, trans_ctm, trans_rect);
+				break;
+			case FZ_CMD_CLIP_STROKE_TEXT:
+				align_node_for_pointer(&node);
+				fz_clip_stroke_text(ctx, dev, *(fz_text **)node, stroke, trans_ctm, trans_rect);
+				break;
+			case FZ_CMD_IGNORE_TEXT:
+				align_node_for_pointer(&node);
+				fz_ignore_text(ctx, dev, *(fz_text **)node, trans_ctm);
+				break;
+			case FZ_CMD_FILL_SHADE:
+				fz_unpack_color_params(&color_params, n.flags);
+				align_node_for_pointer(&node);
+				fz_fill_shade(ctx, dev, *(fz_shade **)node, trans_ctm, alpha, color_params);
+				break;
+			case FZ_CMD_FILL_IMAGE:
+				fz_unpack_color_params(&color_params, n.flags);
+				align_node_for_pointer(&node);
+				fz_fill_image(ctx, dev, *(fz_image **)node, trans_ctm, alpha, color_params);
+				break;
+			case FZ_CMD_FILL_IMAGE_MASK:
+				fz_unpack_color_params(&color_params, n.flags);
+				align_node_for_pointer(&node);
+				fz_fill_image_mask(ctx, dev, *(fz_image **)node, trans_ctm, colorspace, color, alpha, color_params);
+				break;
+			case FZ_CMD_CLIP_IMAGE_MASK:
+				align_node_for_pointer(&node);
+				fz_clip_image_mask(ctx, dev, *(fz_image **)node, trans_ctm, trans_rect);
+				break;
+			case FZ_CMD_POP_CLIP:
+				fz_pop_clip(ctx, dev);
+				break;
+			case FZ_CMD_BEGIN_MASK:
+				fz_unpack_color_params(&color_params, n.flags);
+				fz_begin_mask(ctx, dev, trans_rect, n.flags & 1, colorspace, color, color_params);
+				break;
+			case FZ_CMD_END_MASK:
+				align_node_for_pointer(&node);
+				fz_end_mask_tr(ctx, dev, *(fz_function **)node);
+				break;
+			case FZ_CMD_BEGIN_GROUP:
+				fz_begin_group(ctx, dev, trans_rect, colorspace, (n.flags & ISOLATED) != 0, (n.flags & KNOCKOUT) != 0, (n.flags>>2), alpha);
+				break;
+			case FZ_CMD_END_GROUP:
+				fz_end_group(ctx, dev);
+				break;
+			case FZ_CMD_BEGIN_TILE:
+			{
+				int cached;
+				fz_list_tile_data *data;
+				fz_rect tile_rect;
+				data = (fz_list_tile_data *)node;
+				tiled++;
+				tile_rect = data->view;
+				cached = fz_begin_tile_tid(ctx, dev, rect, tile_rect, data->xstep, data->ystep, trans_ctm, data->id, data->doc_id);
+				if (cached)
+					tile_skip_depth = 1;
+				break;
+			}
+			case FZ_CMD_END_TILE:
+				tiled--;
+				fz_end_tile(ctx, dev);
+				break;
+			case FZ_CMD_RENDER_FLAGS:
+				if (n.flags == 0)
+					fz_render_flags(ctx, dev, 0, FZ_DEVFLAG_GRIDFIT_AS_TILED);
+				else if (n.flags == 1)
+					fz_render_flags(ctx, dev, FZ_DEVFLAG_GRIDFIT_AS_TILED, 0);
+				break;
+			case FZ_CMD_DEFAULT_COLORSPACES:
+				align_node_for_pointer(&node);
+				fz_set_default_colorspaces(ctx, dev, *(fz_default_colorspaces **)node);
+				break;
+			case FZ_CMD_BEGIN_LAYER:
+				fz_begin_layer(ctx, dev, (const char *)node);
+				break;
+			case FZ_CMD_END_LAYER:
+				fz_end_layer(ctx, dev);
+				break;
+			case FZ_CMD_BEGIN_STRUCTURE:
+			{
+				const unsigned char *data;
+				int idx;
+				data = (const unsigned char *)node;
+				memcpy(&idx, data+1, sizeof(idx));
+				fz_begin_structure(ctx, dev, (fz_structure)data[0], (const char *)(&data[1+sizeof(idx)]), idx);
+				break;
+			}
+			case FZ_CMD_END_STRUCTURE:
+				fz_end_structure(ctx, dev);
+				break;
+			case FZ_CMD_BEGIN_METATEXT:
+			{
+				const unsigned char *data;
+				const char *text;
+				data = (const unsigned char *)node;
+				text = (const char *)&data[1];
+				fz_begin_metatext(ctx, dev, (fz_metatext)data[0], text);
+				break;
+			}
+			case FZ_CMD_END_METATEXT:
+				fz_end_metatext(ctx, dev);
+				break;
+			}
+		}
+		fz_catch(ctx)
+		{
+			if (fz_caught(ctx) == FZ_ERROR_SYSTEM)
+			{
+				fz_drop_colorspace(ctx, colorspace);
+				fz_drop_stroke_state(ctx, stroke);
+				fz_drop_path(ctx, path);
+				fz_rethrow(ctx);
+			}
+			/* Swallow the error */
+			if (cookie)
+				cookie->errors++;
+			if (fz_caught(ctx) == FZ_ERROR_ABORT)
+			{
+				fz_ignore_error(ctx);
+				break;
+			}
+			fz_report_error(ctx);
+			fz_warn(ctx, "Ignoring error during interpretation");
+		}
+	}
+	fz_drop_colorspace(ctx, colorspace);
+	fz_drop_stroke_state(ctx, stroke);
+	fz_drop_path(ctx, path);
+	if (cookie)
+		cookie->progress = progress;
+}