[gs-commits] mupdf 1.16.1.epub-prerelease-19 Rework PDF filtering API.

[email protected] (Tor Andersson) Thu, 31 Oct 2019 12:53:37 +0000 (UTC)
Newsgroups gmane.comp.printing.ghostscript.cvs
Message-ID <[email protected]>
commit 15439869a781bbf1fe99ec08dd33af15daf67e4a
Author: Tor Andersson <[email protected]>
Date:   Wed Sep 11 13:33:37 2019 +0200

    Rework PDF filtering API.
    
    Use a pdf_filter_options struct instead of passing half a dozen arguments.
    
    Add image callback to PDF filter processor.
    
    Clean XObject Form resources recursively.
    
    We avoid recursive cycles, but make no effort to avoid cleaning the same
    resource multiple times.
    
    Create instanced XObject forms, specialized per call site, for redaction.

diff --git a/include/mupdf/pdf/interpret.h b/include/mupdf/pdf/interpret.h
index 9974d51..2d33c35 100644
--- a/include/mupdf/pdf/interpret.h
+++ b/include/mupdf/pdf/interpret.h
@@ -171,14 +171,49 @@ pdf_processor *pdf_new_buffer_processor(fz_context *ctx, fz_buffer *buffer, int
 
 pdf_processor *pdf_new_output_processor(fz_context *ctx, fz_output *out, int ahxencode);
 
-pdf_processor *pdf_new_filter_processor(fz_context *ctx, pdf_document *doc, pdf_processor *chain, pdf_obj *old_res, pdf_obj *new_res);
+/*
+	opaque: Opaque value that is passed to all the filter functions.
 
-typedef int (pdf_text_filter_fn)(fz_context *ctx, void *opaque, int *ucsbuf, int ucslen, fz_matrix trm, fz_matrix ctm, fz_rect bbox);
+	image_filter: A function called to assess whether a given
+	image should be removed or not.
 
-typedef void (pdf_after_text_object_fn)(fz_context *ctx, void *opaque, pdf_document *doc, pdf_processor *chain, fz_matrix ctm);
+	text_filter: A function called to assess whether a given
+	character should be removed or not.
 
-pdf_processor *
-pdf_new_filter_processor_with_text_filter(fz_context *ctx, pdf_document *doc, int structparents, pdf_processor *chain, pdf_obj *old_rdb, pdf_obj *new_rdb, pdf_text_filter_fn *text_filter, pdf_after_text_object_fn *after, void *text_filter_opaque);
+	after_text_object: A function called after each text object.
+	This allows the caller to insert some extra content if
+	desired.
+
+	end_page: A function called at the end of a page.
+	This allows the caller to insert some extra content after
+	all other content.
+
+	sanitize: If false, will only clean the syntax. This disables all filtering!
+
+	recurse: Clean/sanitize/filter resources recursively.
+
+	instance_forms: Always recurse on XObject Form resources, but will
+	create a new instance of each XObject Form that is used, filtered
+	individually.
+
+	ascii: If true, escape all binary data in the output.
+*/
+typedef struct pdf_filter_options_s
+{
+	void *opaque;
+	int (*image_filter)(fz_context *ctx, void *opaque, fz_matrix ctm, const char *name, fz_image *image);
+	int (*text_filter)(fz_context *ctx, void *opaque, int *ucsbuf, int ucslen, fz_matrix trm, fz_matrix ctm, fz_rect bbox);
+	void (*after_text_object)(fz_context *ctx, void *opaque, pdf_document *doc, pdf_processor *chain, fz_matrix ctm);
+	void (*end_page)(fz_context *ctx, fz_buffer *buffer, void *arg);
+
+	int recurse;
+	int instance_forms;
+	int sanitize;
+	int ascii;
+} pdf_filter_options;
+
+pdf_processor *pdf_new_filter_processor(fz_context *ctx, pdf_document *doc, pdf_processor *chain, pdf_obj *old_res, pdf_obj *new_res, int struct_parents, fz_matrix transform, pdf_filter_options *filter);
+pdf_obj *pdf_filter_xobject_instance(fz_context *ctx, pdf_obj *old_xobj, pdf_obj *page_res, fz_matrix ctm, pdf_filter_options *filter);
 
 void pdf_process_contents(fz_context *ctx, pdf_processor *proc, pdf_document *doc, pdf_obj *obj, pdf_obj *res, fz_cookie *cookie);
 void pdf_process_annot(fz_context *ctx, pdf_processor *proc, pdf_document *doc, pdf_page *page, pdf_annot *annot, fz_cookie *cookie);
diff --git a/include/mupdf/pdf/page.h b/include/mupdf/pdf/page.h
index 4926a65..6f0cc5c 100644
--- a/include/mupdf/pdf/page.h
+++ b/include/mupdf/pdf/page.h
@@ -41,32 +41,8 @@ void pdf_run_page_contents(fz_context *ctx, pdf_page *page, fz_device *dev, fz_m
 void pdf_run_page_annots(fz_context *ctx, pdf_page *page, fz_device *dev, fz_matrix ctm, fz_cookie *cookie);
 void pdf_run_page_widgets(fz_context *ctx, pdf_page *page, fz_device *dev, fz_matrix ctm, fz_cookie *cookie);
 
-/*
-	A function used for processing the
-	cleaned page contents/resources gathered as part of
-	pdf_clean_page_contents.
-
-	buffer: A buffer holding the page contents.
-
-	res: A pdf_obj holding the page resources.
-
-	arg: An opaque arg specific to the particular function.
-*/
-typedef void (pdf_page_contents_process_fn)(fz_context *ctx, fz_buffer *buffer, pdf_obj *res, void *arg);
-
-void pdf_clean_page_contents(fz_context *ctx, pdf_document *doc, pdf_page *page, fz_cookie *cookie,
-	pdf_page_contents_process_fn *proc, void *proc_arg, int sanitize, int ascii);
-
-void pdf_clean_annot_contents(fz_context *ctx, pdf_document *doc, pdf_annot *annot, fz_cookie *cookie,
-	pdf_page_contents_process_fn *proc, void *proc_arg, int sanitize, int ascii);
-
-void pdf_filter_page_contents(fz_context *ctx, pdf_document *doc, pdf_page *page, fz_cookie *cookie,
-	pdf_page_contents_process_fn *proc_fn, pdf_text_filter_fn *text_filter, pdf_after_text_object_fn *after_text, void *arg,
-	int sanitize, int ascii);
-
-void pdf_filter_annot_contents(fz_context *ctx, pdf_document *doc, pdf_annot *annot, fz_cookie *cookie,
-	pdf_page_contents_process_fn *proc, pdf_text_filter_fn *text_filter, pdf_after_text_object_fn *after_text, void *arg,
-	int sanitize, int ascii);
+void pdf_filter_page_contents(fz_context *ctx, pdf_document *doc, pdf_page *page, pdf_filter_options *filter);
+void pdf_filter_annot_contents(fz_context *ctx, pdf_document *doc, pdf_annot *annot, pdf_filter_options *filter);
 
 typedef struct pdf_redact_options_s pdf_redact_options;
 
diff --git a/source/pdf/pdf-clean.c b/source/pdf/pdf-clean.c
index 0d3f639..5c9e608 100644
--- a/source/pdf/pdf-clean.c
+++ b/source/pdf/pdf-clean.c
@@ -1,112 +1,222 @@
 #include "mupdf/fitz.h"
 #include "mupdf/pdf.h"
 
+#include <string.h>
+#include <assert.h>
+
+static void
+pdf_filter_xobject(fz_context *ctx, pdf_document *doc, pdf_obj *xobj, pdf_obj *page_res, pdf_filter_options *filter);
+
 static void
-pdf_clean_stream_object(fz_context *ctx, pdf_document *doc, pdf_obj *obj, pdf_obj *orig_res, fz_cookie *cookie, int own_res,
-		pdf_text_filter_fn *text_filter, pdf_after_text_object_fn *after_text, void *arg,
-		int sanitize, int ascii)
+pdf_filter_type3(fz_context *ctx, pdf_document *doc, pdf_obj *obj, pdf_obj *page_res, pdf_filter_options *filter);
+
+static void
+pdf_filter_resources(fz_context *ctx, pdf_document *doc, pdf_obj *in_res, pdf_obj *res, pdf_filter_options *filter)
 {
-	pdf_processor *proc_buffer = NULL;
-	pdf_processor *proc_filter = NULL;
-	pdf_obj *res = NULL;
-	pdf_obj *ref;
-	fz_buffer *buffer;
+	pdf_obj *obj;
+	int i, n;
 
-	if (!obj)
+	if (!filter->recurse)
 		return;
 
-	fz_var(res);
-	fz_var(proc_buffer);
-	fz_var(proc_filter);
+	/* ExtGState */
+	obj = pdf_dict_get(ctx, res, PDF_NAME(ExtGState));
+	if (obj)
+	{
+		n = pdf_dict_len(ctx, obj);
+		for (i = 0; i < n; i++)
+		{
+			pdf_obj *smask = pdf_dict_get(ctx, pdf_dict_get_val(ctx, obj, i), PDF_NAME(SMask));
+			if (smask)
+			{
+				pdf_obj *g = pdf_dict_get(ctx, smask, PDF_NAME(G));
+				if (g)
+				{
+					/* Transparency group XObject */
+					pdf_filter_xobject(ctx, doc, g, in_res, filter);
+				}
+			}
+		}
+	}
 
-	buffer = fz_new_buffer(ctx, 1024);
+	/* Pattern */
+	obj = pdf_dict_get(ctx, res, PDF_NAME(Pattern));
+	if (obj)
+	{
+		n = pdf_dict_len(ctx, obj);
+		for (i = 0; i < n; i++)
+		{
+			pdf_obj *pat = pdf_dict_get_val(ctx, obj, i);
+			if (pat && pdf_dict_get_int(ctx, pat, PDF_NAME(PatternType)) == 1)
+			{
+				pdf_filter_xobject(ctx, doc, pat, in_res, filter);
+			}
+		}
+	}
 
-	fz_try(ctx)
+	/* XObject */
+	if (!filter->instance_forms)
 	{
-		pdf_obj *sp = pdf_dict_get(ctx, obj, PDF_NAME(StructParents));
-		int structparents = -1;
-		if (pdf_is_number(ctx, sp))
-			structparents = pdf_to_int(ctx, sp);
+		obj = pdf_dict_get(ctx, res, PDF_NAME(XObject));
+		if (obj)
+		{
+			n = pdf_dict_len(ctx, obj);
+			for (i = 0; i < n; i++)
+			{
+				pdf_obj *xobj = pdf_dict_get_val(ctx, obj, i);
+				if (xobj && pdf_dict_get(ctx, xobj, PDF_NAME(Subtype)) == PDF_NAME(Form))
+				{
+					pdf_filter_xobject(ctx, doc, xobj, in_res, filter);
+				}
+			}
+		}
+	}
 
-		if (own_res)
+	/* Font */
+	obj = pdf_dict_get(ctx, res, PDF_NAME(Font));
+	if (obj)
+	{
+		n = pdf_dict_len(ctx, obj);
+		for (i = 0; i < n; i++)
 		{
-			pdf_obj *r = pdf_dict_get(ctx, obj, PDF_NAME(Resources));
-			if (r)
-				orig_res = r;
+			pdf_obj *font = pdf_dict_get_val(ctx, obj, i);
+			if (font && pdf_dict_get(ctx, font, PDF_NAME(Subtype)) == PDF_NAME(Type3))
+			{
+				pdf_filter_type3(ctx, doc, font, in_res, filter);
+			}
 		}
+	}
 
-		res = pdf_new_dict(ctx, doc, 1);
+}
 
-		proc_buffer = pdf_new_buffer_processor(ctx, buffer, ascii);
-		proc_filter = pdf_new_filter_processor_with_text_filter(ctx, doc, structparents, proc_buffer, orig_res, res, text_filter, after_text, arg);
+/*
+	Clean a content stream's rendering operations, with an optional post
+	processing step.
 
-		pdf_process_contents(ctx, proc_filter, doc, orig_res, obj, cookie);
-		pdf_close_processor(ctx, proc_filter);
-		pdf_close_processor(ctx, proc_buffer);
+	Firstly, this filters the PDF operators used to avoid (some cases of)
+	repetition, and leaves the content stream in a balanced state with an
+	unchanged top level matrix etc. At the same time, the resources actually
+	used are collected into a new resource dictionary.
 
-		pdf_update_stream(ctx, doc, obj, buffer, 0);
+	Next, the resources themselves are recursively cleaned (as appropriate)
+	in the same way, if the 'recurse' flag is set.
+*/
+static void
+pdf_filter_content_stream(
+	fz_context *ctx,
+	pdf_document *doc,
+	pdf_obj *in_stm,
+	pdf_obj *in_res,
+	fz_matrix transform,
+	pdf_filter_options *filter,
+	int struct_parents,
+	fz_buffer **out_buf,
+	pdf_obj **out_res)
+{
+	pdf_processor *proc_buffer = NULL;
+	pdf_processor *proc_filter = NULL;
+
+	fz_var(proc_buffer);
+	fz_var(proc_filter);
+
+	*out_buf = NULL;
+	*out_res = NULL;
 
-		if (own_res)
+	fz_try(ctx)
+	{
+		*out_buf = fz_new_buffer(ctx, 1024);
+		proc_buffer = pdf_new_buffer_processor(ctx, *out_buf, filter->ascii);
+		if (filter->sanitize)
+		{
+			*out_res = pdf_new_dict(ctx, doc, 1);
+			proc_filter = pdf_new_filter_processor(ctx, doc, proc_buffer, in_res, *out_res, struct_parents, transform, filter);
+			pdf_process_contents(ctx, proc_filter, doc, in_res, in_stm, NULL);
+			pdf_close_processor(ctx, proc_filter);
+		}
+		else
 		{
-			ref = pdf_add_object(ctx, doc, res);
-			pdf_dict_put_drop(ctx, obj, PDF_NAME(Resources), ref);
+			*out_res = pdf_keep_obj(ctx, in_res);
+			pdf_process_contents(ctx, proc_buffer, doc, in_res, in_stm, NULL);
 		}
+		pdf_close_processor(ctx, proc_buffer);
+
+		pdf_filter_resources(ctx, doc, in_res, *out_res, filter);
 	}
 	fz_always(ctx)
 	{
 		pdf_drop_processor(ctx, proc_filter);
 		pdf_drop_processor(ctx, proc_buffer);
-		fz_drop_buffer(ctx, buffer);
-		pdf_drop_obj(ctx, res);
 	}
 	fz_catch(ctx)
 	{
+		fz_drop_buffer(ctx, *out_buf);
+		*out_buf = NULL;
+		pdf_drop_obj(ctx, *out_res);
+		*out_res = NULL;
 		fz_rethrow(ctx);
 	}
 }
 
+/*
+	Clean a Type 3 font's CharProcs content streams. This works almost
+	exactly like pdf_filter_content_stream, but the resource dictionary is
+	shared between all off the CharProcs.
+*/
 static void
-pdf_clean_type3(fz_context *ctx, pdf_document *doc, pdf_obj *obj, pdf_obj *orig_res, fz_cookie *cookie, int sanitize, int ascii)
+pdf_filter_type3(fz_context *ctx, pdf_document *doc, pdf_obj *obj, pdf_obj *page_res, pdf_filter_options *filter)
 {
 	pdf_processor *proc_buffer = NULL;
 	pdf_processor *proc_filter = NULL;
-	pdf_obj *res = NULL;
-	pdf_obj *ref;
+	pdf_obj *in_res;
+	pdf_obj *out_res = NULL;
 	pdf_obj *charprocs;
-	int i, l;
+	int i, n;
 
-	fz_var(res);
+	fz_var(out_res);
 	fz_var(proc_buffer);
 	fz_var(proc_filter);
 
+	/* We cannot combine instancing with type3 fonts. The new names for
+	 * instanced form/image resources would clash, since they start over for
+	 * each content stream. This is not a problem for now, because we only
+	 * use instancing with redaction, and redaction doesn't clean type3
+	 * fonts.
+	 */
+	assert(!filter->instance_forms);
+
+	/* Avoid recursive cycles! */
+	if (pdf_mark_obj(ctx, obj))
+		return;
+
 	fz_try(ctx)
 	{
-		res = pdf_dict_get(ctx, obj, PDF_NAME(Resources));
-		if (res)
-			orig_res = res;
-		res = NULL;
+		in_res = pdf_dict_get(ctx, obj, PDF_NAME(Resources));
+		if (!in_res)
+			in_res = page_res;
 
-		res = pdf_new_dict(ctx, doc, 1);
+		if (filter->sanitize)
+			out_res = pdf_new_dict(ctx, doc, 1);
+		else
+			out_res = pdf_keep_obj(ctx, in_res);
 
 		charprocs = pdf_dict_get(ctx, obj, PDF_NAME(CharProcs));
-		l = pdf_dict_len(ctx, charprocs);
-
-		for (i = 0; i < l; i++)
+		n = pdf_dict_len(ctx, charprocs);
+		for (i = 0; i < n; i++)
 		{
 			pdf_obj *val = pdf_dict_get_val(ctx, charprocs, i);
 			fz_buffer *buffer = fz_new_buffer(ctx, 1024);
 			fz_try(ctx)
 			{
-				proc_buffer = pdf_new_buffer_processor(ctx, buffer, ascii);
-				if (sanitize)
+				proc_buffer = pdf_new_buffer_processor(ctx, buffer, filter->ascii);
+				if (filter->sanitize)
 				{
-					proc_filter = pdf_new_filter_processor(ctx, doc, proc_buffer, orig_res, res);
-					pdf_process_contents(ctx, proc_filter, doc, orig_res, val, cookie);
+					proc_filter = pdf_new_filter_processor(ctx, doc, proc_buffer, in_res, out_res, -1, fz_identity, filter);
+					pdf_process_contents(ctx, proc_filter, doc, in_res, val, NULL);
 					pdf_close_processor(ctx, proc_filter);
 				}
 				else
 				{
-					pdf_process_contents(ctx, proc_filter, doc, orig_res, val, cookie);
+					pdf_process_contents(ctx, proc_buffer, doc, in_res, val, NULL);
 				}
 				pdf_close_processor(ctx, proc_buffer);
 
@@ -124,15 +234,15 @@ pdf_clean_type3(fz_context *ctx, pdf_document *doc, pdf_obj *obj, pdf_obj *orig_
 			}
 		}
 
-		/* ProcSet - no cleaning possible. Inherit this from the old dict. */
-		pdf_dict_put(ctx, res, PDF_NAME(ProcSet), pdf_dict_get(ctx, orig_res, PDF_NAME(ProcSet)));
+		pdf_filter_resources(ctx, doc, in_res, out_res, filter);
 
-		ref = pdf_add_object(ctx, doc, res);
-		pdf_dict_put_drop(ctx, obj, PDF_NAME(Resources), ref);
+		if (filter->sanitize)
+			pdf_dict_put(ctx, obj, PDF_NAME(Resources), out_res);
 	}
 	fz_always(ctx)
 	{
-		pdf_drop_obj(ctx, res);
+		pdf_unmark_obj(ctx, obj);
+		pdf_drop_obj(ctx, out_res);
 	}
 	fz_catch(ctx)
 	{
@@ -140,291 +250,160 @@ pdf_clean_type3(fz_context *ctx, pdf_document *doc, pdf_obj *obj, pdf_obj *orig_
 	}
 }
 
-/*
-	Clean a loaded pages rendering operations,
-	with an optional post processing step.
-
-	Firstly, this filters the PDF operators used to avoid (some cases
-	of) repetition, and leaves the page in a balanced state with an
-	unchanged top level matrix etc. At the same time, the resources
-	used by the page contents are collected.
-
-	Next, the resources themselves are cleaned (as appropriate) in the
-	same way.
-
-	Next, an optional post processing stage is called.
-
-	Finally, the page contents and resources in the documents page tree
-	are replaced by these processed versions.
-
-	Annotations remain unaffected.
-
-	page: A page loaded by pdf_load_page.
-
-	cookie: A pointer to an optional fz_cookie structure that can be used
-	to track progress, collect errors etc.
-*/
-void pdf_clean_page_contents(fz_context *ctx, pdf_document *doc, pdf_page *page, fz_cookie *cookie, pdf_page_contents_process_fn *proc_fn, void *arg, int sanitize, int ascii)
+static void
+pdf_filter_xobject(fz_context *ctx, pdf_document *doc, pdf_obj *stm, pdf_obj *page_res, pdf_filter_options *filter)
 {
-	pdf_filter_page_contents(ctx, doc, page, cookie, proc_fn, NULL, NULL, arg, sanitize, ascii);
-}
+	pdf_obj *struct_parents_obj;
+	int struct_parents;
+	pdf_obj *new_res = NULL;
+	fz_buffer *new_buf = NULL;
+	pdf_obj *old_res;
 
-/*
-	Performs the same task as
-	pdf_clean_page_contents, but with an optional text filter
-	function.
+	fz_var(new_buf);
+	fz_var(new_res);
 
-	text_filter: Function to assess whether a given character
-	should be kept (return 0) or removed (return 1).
+	// TODO for RJW: XObject can also be a StructParent; how do we handle that case?
 
-	after_text: Function called after each text object is closed
-	to allow other output to be sent.
+	struct_parents_obj = pdf_dict_get(ctx, stm, PDF_NAME(StructParents));
+	struct_parents = -1;
+	if (pdf_is_number(ctx, struct_parents_obj))
+		struct_parents = pdf_to_int(ctx, struct_parents_obj);
 
-	arg: Opaque value to be passed to callback functions.
-*/
-void pdf_filter_page_contents(fz_context *ctx, pdf_document *doc, pdf_page *page, fz_cookie *cookie,
-		pdf_page_contents_process_fn *proc_fn, pdf_text_filter_fn *text_filter, pdf_after_text_object_fn *after_text, void *proc_arg,
-		int sanitize, int ascii)
-{
-	pdf_processor *proc_buffer = NULL;
-	pdf_processor *proc_filter = NULL;
-	pdf_obj *new_obj = NULL;
-	pdf_obj *new_ref = NULL;
-	pdf_obj *res = NULL;
-	pdf_obj *obj;
-	pdf_obj *contents;
-	pdf_obj *resources;
-	fz_buffer *buffer;
+	old_res = pdf_dict_get(ctx, stm, PDF_NAME(Resources));
+	if (!old_res)
+		old_res = page_res;
 
-	fz_var(new_obj);
-	fz_var(new_ref);
-	fz_var(res);
-	fz_var(proc_buffer);
-	fz_var(proc_filter);
-
-	buffer = fz_new_buffer(ctx, 1024);
+	// TODO: don't clean objects more than once.
 
+	/* Avoid recursive cycles! */
+	if (pdf_mark_obj(ctx, stm))
+		return;
 	fz_try(ctx)
 	{
-		pdf_obj *sp = pdf_dict_get(ctx, page->obj, PDF_NAME(StructParents));
-		int structparents = -1;
-		if (pdf_is_number(ctx, sp))
-			structparents = pdf_to_int(ctx, sp);
-		contents = pdf_page_contents(ctx, page);
-		resources = pdf_page_resources(ctx, page);
-
-		proc_buffer = pdf_new_buffer_processor(ctx, buffer, ascii);
-		if (sanitize)
-		{
-			res = pdf_new_dict(ctx, doc, 1);
-			proc_filter = pdf_new_filter_processor_with_text_filter(ctx, doc, structparents, proc_buffer, resources, res, text_filter, after_text, proc_arg);
-			pdf_process_contents(ctx, proc_filter, doc, resources, contents, cookie);
-			pdf_close_processor(ctx, proc_filter);
-		}
-		else
-		{
-			res = pdf_keep_obj(ctx, resources);
-			pdf_process_contents(ctx, proc_buffer, doc, resources, contents, cookie);
-		}
-		pdf_close_processor(ctx, proc_buffer);
+		pdf_filter_content_stream(ctx, doc, stm, old_res, fz_identity, filter, struct_parents, &new_buf, &new_res);
+		pdf_update_stream(ctx, doc, stm, new_buf, 0);
+		pdf_dict_put(ctx, stm, PDF_NAME(Resources), new_res);
+	}
+	fz_always(ctx)
+	{
+		pdf_unmark_obj(ctx, stm);
+		fz_drop_buffer(ctx, new_buf);
+		pdf_drop_obj(ctx, new_res);
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
 
-		/* Deal with page content stream. */
+pdf_obj *
+pdf_filter_xobject_instance(fz_context *ctx, pdf_obj *old_xobj, pdf_obj *page_res, fz_matrix transform, pdf_filter_options *filter)
+{
+	pdf_document *doc = pdf_get_bound_document(ctx, old_xobj);
+	pdf_obj *new_xobj;
+	pdf_obj *new_res, *old_res;
+	fz_buffer *new_buf;
+	pdf_obj *struct_parents_obj;
+	int struct_parents;
 
-		if (pdf_is_array(ctx, contents))
-		{
-			/* create a new object to replace the array */
-			new_obj = pdf_new_dict(ctx, doc, 1);
-			new_ref = pdf_add_object(ctx, doc, new_obj);
-			contents = new_ref;
-			pdf_dict_put(ctx, page->obj, PDF_NAME(Contents), contents);
-		}
-		else
-		{
-			pdf_dict_del(ctx, contents, PDF_NAME(Filter));
-			pdf_dict_del(ctx, contents, PDF_NAME(DecodeParms));
-		}
+	fz_var(new_xobj);
+	fz_var(new_buf);
+	fz_var(new_res);
 
-		pdf_update_stream(ctx, doc, contents, buffer, 0);
+	// TODO for RJW: XObject can also be a StructParent; how do we handle that case?
+	// TODO for RJW: will we run into trouble by duplicating StructParents stuff?
 
-		/* Now deal with resources. The spec allows for Type3 fonts and form
-		 * XObjects to omit a resource dictionary and look in the parent.
-		 * Avoid that by flattening here as part of the cleaning. This could
-		 * conceivably cause changes in rendering, but we don't care. */
+	struct_parents_obj = pdf_dict_get(ctx, old_xobj, PDF_NAME(StructParents));
+	struct_parents = -1;
+	if (pdf_is_number(ctx, struct_parents_obj))
+		struct_parents = pdf_to_int(ctx, struct_parents_obj);
 
-		/* ExtGState */
-		obj = pdf_dict_get(ctx, res, PDF_NAME(ExtGState));
-		if (obj)
-		{
-			int i, l;
+	old_res = pdf_dict_get(ctx, old_xobj, PDF_NAME(Resources));
+	if (!old_res)
+		old_res = page_res;
 
-			l = pdf_dict_len(ctx, obj);
-			for (i = 0; i < l; i++)
-			{
-				pdf_obj *o = pdf_dict_get(ctx, pdf_dict_get_val(ctx, obj, i), PDF_NAME(SMask));
-				if (!o)
-					continue;
-				o = pdf_dict_get(ctx, o, PDF_NAME(G));
-				if (!o)
-					continue;
-				/* Transparency group XObject */
-				pdf_clean_stream_object(ctx, doc, o, resources, cookie, 1, text_filter, after_text, proc_arg, sanitize, ascii);
-			}
-		}
+	if (pdf_mark_obj(ctx, old_xobj))
+		return pdf_keep_obj(ctx, old_xobj);
 
-		/* Pattern */
-		obj = pdf_dict_get(ctx, res, PDF_NAME(Pattern));
-		if (obj)
-		{
-			int i, l;
-			l = pdf_dict_len(ctx, obj);
-			for (i = 0; i < l; i++)
-			{
-				pdf_obj *pat_res;
-				pdf_obj *pat = pdf_dict_get_val(ctx, obj, i);
-				if (!pat)
-					continue;
-				pat_res = pdf_dict_get(ctx, pat, PDF_NAME(Resources));
-				if (pat_res == NULL)
-					pat_res = resources;
-				if (pdf_dict_get_int(ctx, pat, PDF_NAME(PatternType)) == 1)
-					pdf_clean_stream_object(ctx, doc, pat, pat_res, cookie, 0, text_filter, after_text, proc_arg, sanitize, ascii);
-			}
-		}
+	fz_try(ctx)
+	{
+		new_xobj = pdf_add_object_drop(ctx, doc, pdf_copy_dict(ctx, old_xobj));
+		pdf_filter_content_stream(ctx, doc, old_xobj, old_res, transform, filter, struct_parents, &new_buf, &new_res);
+		pdf_update_stream(ctx, doc, new_xobj, new_buf, 0);
+		pdf_dict_put(ctx, new_xobj, PDF_NAME(Resources), new_res);
+	}
+	fz_always(ctx)
+	{
+		pdf_unmark_obj(ctx, old_xobj);
+		fz_drop_buffer(ctx, new_buf);
+		pdf_drop_obj(ctx, new_res);
+	}
+	fz_catch(ctx)
+	{
+		pdf_drop_obj(ctx, new_xobj);
+		fz_rethrow(ctx);
+	}
 
-		/* XObject */
-		obj = pdf_dict_get(ctx, res, PDF_NAME(XObject));
-		if (obj)
-		{
-			int i, l;
-			l = pdf_dict_len(ctx, obj);
-			for (i = 0; i < l; i++)
-			{
-				pdf_obj *xobj_res;
-				pdf_obj *xobj = pdf_dict_get_val(ctx, obj, i);
-				if (!xobj)
-					continue;
-				xobj_res = pdf_dict_get(ctx, xobj, PDF_NAME(Resources));
-				if (xobj_res == NULL)
-					xobj_res = resources;
-				if (pdf_name_eq(ctx, PDF_NAME(Form), pdf_dict_get(ctx, xobj, PDF_NAME(Subtype))))
-					pdf_clean_stream_object(ctx, doc, xobj, xobj_res, cookie, 1, text_filter, after_text, proc_arg, sanitize, ascii);
-			}
-		}
+	return new_xobj;
+}
 
-		/* Font */
-		obj = pdf_dict_get(ctx, res, PDF_NAME(Font));
-		if (obj)
-		{
-			int i, l;
-			l = pdf_dict_len(ctx, obj);
-			for (i = 0; i < l; i++)
-			{
-				pdf_obj *o = pdf_dict_get_val(ctx, obj, i);
-				if (!o)
-					continue;
-				if (pdf_name_eq(ctx, PDF_NAME(Type3), pdf_dict_get(ctx, o, PDF_NAME(Subtype))))
-					pdf_clean_type3(ctx, doc, o, resources, cookie, sanitize, ascii);
-			}
-		}
+void pdf_filter_page_contents(fz_context *ctx, pdf_document *doc, pdf_page *page, pdf_filter_options *filter)
+{
+	pdf_obj *contents, *old_res;
+	pdf_obj *struct_parents_obj;
+	pdf_obj *new_res;
+	fz_buffer *buffer;
+	int struct_parents;
 
-		/* ProcSet - no cleaning possible. Inherit this from the old dict. */
-		obj = pdf_dict_get(ctx, resources, PDF_NAME(ProcSet));
-		if (obj)
-			pdf_dict_put(ctx, res, PDF_NAME(ProcSet), obj);
+	struct_parents_obj = pdf_dict_get(ctx, page->obj, PDF_NAME(StructParents));
+	struct_parents = -1;
+	if (pdf_is_number(ctx, struct_parents_obj))
+		struct_parents = pdf_to_int(ctx, struct_parents_obj);
 
-		/* ColorSpace - no cleaning possible. */
-		/* Properties - no cleaning possible. */
+	contents = pdf_page_contents(ctx, page);
+	old_res = pdf_page_resources(ctx, page);
 
-		if (proc_fn)
-			(*proc_fn)(ctx, buffer, res, proc_arg);
+	pdf_filter_content_stream(ctx, doc, contents, old_res, fz_identity, filter, struct_parents, &buffer, &new_res);
 
-		/* Update resource dictionary */
-		if (sanitize)
+	fz_try(ctx)
+	{
+		if (filter->end_page)
+			filter->end_page(ctx, buffer, filter->opaque);
+		if (pdf_is_array(ctx, contents))
 		{
-			pdf_dict_put(ctx, page->obj, PDF_NAME(Resources), res);
+			/* Create a new stream object to replace the array of streams. */
+			contents = pdf_add_object_drop(ctx, doc, pdf_new_dict(ctx, doc, 1));
+			pdf_dict_put_drop(ctx, page->obj, PDF_NAME(Contents), contents);
 		}
+		pdf_update_stream(ctx, doc, contents, buffer, 0);
+		pdf_dict_put(ctx, page->obj, PDF_NAME(Resources), new_res);
 	}
 	fz_always(ctx)
 	{
-		pdf_drop_processor(ctx, proc_filter);
-		pdf_drop_processor(ctx, proc_buffer);
 		fz_drop_buffer(ctx, buffer);
-		pdf_drop_obj(ctx, new_obj);
-		pdf_drop_obj(ctx, new_ref);
-		pdf_drop_obj(ctx, res);
+		pdf_drop_obj(ctx, new_res);
 	}
 	fz_catch(ctx)
-	{
 		fz_rethrow(ctx);
-	}
 }
 
-/*
-	Clean a loaded annotations rendering operations,
-	with an optional post processing step.
-
-	Each appearance stream in the annotation is processed.
-
-	Firstly, this filters the PDF operators used to avoid (some cases
-	of) repetition, and leaves the page in a balanced state with an
-	unchanged top level matrix etc. At the same time, the resources
-	used by the page contents are collected.
-
-	Next, the resources themselves are cleaned (as appropriate) in the
-	same way.
-
-	Next, an optional post processing stage is called.
-
-	Finally, the updated stream of operations is reinserted into the
-	appearance stream.
-
-	annot: An annotation loaded by pdf_load_annot.
-
-	cookie: A pointer to an optional fz_cookie structure that can be used
-	to track progress, collect errors etc.
-*/
-void pdf_clean_annot_contents(fz_context *ctx, pdf_document *doc, pdf_annot *annot, fz_cookie *cookie, pdf_page_contents_process_fn *proc_fn, void *proc_arg, int sanitize, int ascii)
-{
-	pdf_filter_annot_contents(ctx, doc, annot, cookie, proc_fn, NULL, NULL, proc_arg, sanitize, ascii);
-}
-
-/*
-	Performs the same task as
-	pdf_clean_annot_contents, but with an optional text filter
-	function.
-
-	text_filter: Function to assess whether a given character
-	should be kept (return 0) or removed (return 1).
-
-	after_text: Function called after each text object is closed
-	to allow other output to be sent.
-
-	arg: Opaque value to be passed to callback functions.
-*/
-void pdf_filter_annot_contents(fz_context *ctx, pdf_document *doc, pdf_annot *annot, fz_cookie *cookie,
-	pdf_page_contents_process_fn *proc, pdf_text_filter_fn *text_filter, pdf_after_text_object_fn *after_text, void *arg, int sanitize, int ascii)
+void pdf_filter_annot_contents(fz_context *ctx, pdf_document *doc, pdf_annot *annot, pdf_filter_options *filter)
 {
-	pdf_obj *ap;
-	int i, n;
-
-	ap = pdf_dict_get(ctx, annot->obj, PDF_NAME(AP));
-	if (ap == NULL)
-		return;
-
-	n = pdf_dict_len(ctx, ap);
-	for (i = 0; i < n; i++)
+	pdf_obj *ap = pdf_dict_get(ctx, annot->obj, PDF_NAME(AP));
+	if (pdf_is_dict(ctx, ap))
 	{
-		pdf_obj *v = pdf_dict_get_val(ctx, ap, i);
-
-		if (v == NULL)
-			continue;
-
-		pdf_clean_stream_object(ctx, doc, v, NULL, cookie, 1, text_filter, after_text, arg, sanitize, ascii);
+		int i, n = pdf_dict_len(ctx, ap);
+		for (i = 0; i < n; i++)
+		{
+			pdf_obj *stm = pdf_dict_get_val(ctx, ap, i);
+			if (pdf_is_stream(ctx, stm))
+			{
+				pdf_filter_xobject(ctx, doc, stm, NULL, filter);
+			}
+		}
 	}
 }
 
 static void
-pdf_redact_end_page(fz_context *ctx, fz_buffer *buf, pdf_obj *res, void *opaque)
+pdf_redact_end_page(fz_context *ctx, fz_buffer *buf, void *opaque)
 {
 	pdf_page *page = opaque;
 	pdf_annot *annot;
@@ -470,11 +449,13 @@ pdf_redact_text_filter(fz_context *ctx, void *opaque, int *ucsbuf, int ucslen, f
 	pdf_page *page = opaque;
 	pdf_annot *annot;
 	pdf_obj *qp;
+	fz_point p;
 	fz_rect r;
 	fz_quad q;
 	int i, n;
 
 	trm = fz_concat(trm, ctm);
+	p = fz_make_point(trm.e, trm.f);
 
 	for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot))
 	{
@@ -487,14 +468,14 @@ pdf_redact_text_filter(fz_context *ctx, void *opaque, int *ucsbuf, int ucslen, f
 				for (i = 0; i < n; i += 8)
 				{
 					q = pdf_to_quad(ctx, qp, i);
-					if (fz_is_point_inside_quad(fz_make_point(trm.e, trm.f), q))
+					if (fz_is_point_inside_quad(p, q))
 						return 1;
 				}
 			}
 			else
 			{
 				r = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect));
-				if (fz_is_point_inside_rect(fz_make_point(trm.e, trm.f), r))
+				if (fz_is_point_inside_rect(p, r))
 					return 1;
 			}
 		}
@@ -510,24 +491,29 @@ pdf_redact_page(fz_context *ctx, pdf_document *doc, pdf_page *page, pdf_redact_o
 	int has_redactions = 0;
 	int no_black_boxes = 0;
 
+	pdf_filter_options filter;
+
 	if (opts)
 	{
 		no_black_boxes = opts->no_black_boxes;
 	}
 
+	memset(&filter, 0, sizeof filter);
+	filter.opaque = page;
+	filter.text_filter = pdf_redact_text_filter;
+	if (!no_black_boxes)
+		filter.end_page = pdf_redact_end_page;
+	filter.recurse = 0; /* don't redact patterns, softmasks, and type3 fonts */
+	filter.instance_forms = 1; /* redact xobjects with instancing */
+	filter.sanitize = 1;
+	filter.ascii = 1;
+
 	for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot))
 		if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact))
 			has_redactions = 1;
 
 	if (has_redactions)
-	{
-		pdf_filter_page_contents(ctx, doc, page, NULL,
-			no_black_boxes ? NULL : pdf_redact_end_page,
-			pdf_redact_text_filter,
-			NULL,
-			page,
-			1, 1);
-	}
+		pdf_filter_page_contents(ctx, doc, page, &filter);
 
 	annot = pdf_first_annot(ctx, page);
 	while (annot)
diff --git a/source/pdf/pdf-op-filter.c b/source/pdf/pdf-op-filter.c
index 70a58db..d58f755 100644
--- a/source/pdf/pdf-op-filter.c
+++ b/source/pdf/pdf-op-filter.c
@@ -90,10 +90,11 @@ typedef struct pdf_filter_processor_s
 	void *font_name;
 	tag_record *current_tags;
 	tag_record *pending_tags;
-	pdf_text_filter_fn *text_filter;
-	pdf_after_text_object_fn *after_text;
-	void *opaque;
 	pdf_obj *old_rdb, *new_rdb;
+	pdf_filter_options *filter;
+	fz_matrix transform;
+	int form_id;
+	int image_id;
 } pdf_filter_processor;
 
 static void
@@ -119,6 +120,15 @@ copy_resource(fz_context *ctx, pdf_filter_processor *p, pdf_obj *key, const char
 }
 
 static void
+add_resource(fz_context *ctx, pdf_filter_processor *p, pdf_obj *key, const char *name, pdf_obj *val)
+{
+	pdf_obj *res = pdf_dict_get(ctx, p->new_rdb, key);
+	if (!res)
+		res = pdf_dict_put_dict(ctx, p->new_rdb, key, 8);
+	pdf_dict_puts(ctx, res, name, val);
+}
+
+static void
 filter_push(fz_context *ctx, pdf_filter_processor *p)
 {
 	filter_gstate *gstate = p->gstate;
@@ -501,11 +511,14 @@ filter_show_char(fz_context *ctx, pdf_filter_processor *p, int cid, int *unicode
 	}
 	*unicode = ucsbuf[0];
 
-	if (p->text_filter)
+	if (p->filter->text_filter)
 	{
-		fz_matrix ctm = fz_concat(gstate->pending.ctm, gstate->sent.ctm);
+		fz_matrix ctm;
 		fz_rect bbox;
 
+		ctm = fz_concat(gstate->pending.ctm, gstate->sent.ctm);
+		ctm = fz_concat(ctm, p->transform);
+
 		if (fontdesc->wmode == 0)
 		{
 			bbox.x0 = 0;
@@ -522,7 +535,7 @@ filter_show_char(fz_context *ctx, pdf_filter_processor *p, int cid, int *unicode
 			bbox.y1 = fz_advance_glyph(ctx, fontdesc->font, p->tos.gid, 1);
 		}
 
-		remove = p->text_filter(ctx, p->opaque, ucsbuf, ucslen, trm, ctm, bbox);
+		remove = p->filter->text_filter(ctx, p->filter->opaque, ucsbuf, ucslen, trm, ctm, bbox);
 	}
 
 	pdf_tos_move_after_char(ctx, &p->tos);
@@ -1263,12 +1276,14 @@ pdf_filter_ET(fz_context *ctx, pdf_processor *proc)
 			p->chain->op_ET(ctx, p->chain);
 	}
 	p->BT_pending = 0;
-	if (p->after_text)
+	if (p->filter->after_text_object)
 	{
-		fz_matrix ctm = fz_concat(p->gstate->pending.ctm, p->gstate->sent.ctm);
+		fz_matrix ctm;
+		ctm = fz_concat(p->gstate->pending.ctm, p->gstate->sent.ctm);
+		ctm = fz_concat(ctm, p->transform);
 		if (p->chain->op_q)
 			p->chain->op_q(ctx, p->chain);
-		p->after_text(ctx, p->opaque, p->doc, p->chain, ctm);
+		p->filter->after_text_object(ctx, p->filter->opaque, p->doc, p->chain, ctm);
 		if (p->chain->op_Q)
 			p->chain->op_Q(ctx, p->chain);
 	}
@@ -1604,12 +1619,16 @@ pdf_filter_k(fz_context *ctx, pdf_processor *proc, float c, float m, float y, fl
 /* shadings, images, xobjects */
 
 static void
-pdf_filter_BI(fz_context *ctx, pdf_processor *proc, fz_image *img, const char *colorspace)
+pdf_filter_BI(fz_context *ctx, pdf_processor *proc, fz_image *image, const char *colorspace)
 {
 	pdf_filter_processor *p = (pdf_filter_processor*)proc;
+	fz_matrix ctm;
 	filter_flush(ctx, p, FLUSH_ALL);
+	ctm = fz_concat(p->gstate->sent.ctm, p->transform);
+	if (p->filter->image_filter && p->filter->image_filter(ctx, p->filter->opaque, ctm, "<inline>", image))
+		return;
 	if (p->chain->op_BI)
-		p->chain->op_BI(ctx, p->chain, img, colorspace);
+		p->chain->op_BI(ctx, p->chain, image, colorspace);
 }
 
 static void
@@ -1626,20 +1645,61 @@ static void
 pdf_filter_Do_image(fz_context *ctx, pdf_processor *proc, const char *name, fz_image *image)
 {
 	pdf_filter_processor *p = (pdf_filter_processor*)proc;
+	fz_matrix ctm;
 	filter_flush(ctx, p, FLUSH_ALL);
-	if (p->chain->op_Do_image)
-		p->chain->op_Do_image(ctx, p->chain, name, image);
-	copy_resource(ctx, p, PDF_NAME(XObject), name);
+	ctm = fz_concat(p->gstate->sent.ctm, p->transform);
+	if (p->filter->image_filter && p->filter->image_filter(ctx, p->filter->opaque, ctm, name, image))
+		return;
+	if (p->filter->instance_forms)
+	{
+		/* Make up a unique name when instancing forms so we don't accidentally clash. */
+		char buf[40];
+		pdf_obj *obj = pdf_dict_gets(ctx, pdf_dict_get(ctx, p->old_rdb, PDF_NAME(XObject)), name);
+		fz_snprintf(buf, sizeof buf, "Im%d", p->image_id++);
+		add_resource(ctx, p, PDF_NAME(XObject), buf, obj);
+		if (p->chain->op_Do_image)
+			p->chain->op_Do_image(ctx, p->chain, buf, image);
+	}
+	else
+	{
+		copy_resource(ctx, p, PDF_NAME(XObject), name);
+		if (p->chain->op_Do_image)
+			p->chain->op_Do_image(ctx, p->chain, name, image);
+	}
 }
 
 static void
 pdf_filter_Do_form(fz_context *ctx, pdf_processor *proc, const char *name, pdf_obj *xobj, pdf_obj *page_resources)
 {
 	pdf_filter_processor *p = (pdf_filter_processor*)proc;
+	fz_matrix transform;
 	filter_flush(ctx, p, FLUSH_ALL);
-	if (p->chain->op_Do_form)
-		p->chain->op_Do_form(ctx, p->chain, name, xobj, page_resources);
-	copy_resource(ctx, p, PDF_NAME(XObject), name);
+
+	if (p->filter->instance_forms)
+	{
+		/* Copy an instance of the form with a new unique name. */
+		pdf_obj *new_xobj;
+		char buf[40];
+		fz_snprintf(buf, sizeof buf, "Fm%d", p->form_id++);
+		transform = fz_concat(p->gstate->sent.ctm, p->transform);
+		new_xobj = pdf_filter_xobject_instance(ctx, xobj, page_resources, transform, p->filter);
+		fz_try(ctx)
+		{
+			add_resource(ctx, p, PDF_NAME(XObject), buf, new_xobj);
+			if (p->chain->op_Do_form)
+				p->chain->op_Do_form(ctx, p->chain, buf, new_xobj, page_resources);
+		}
+		fz_always(ctx)
+			pdf_drop_obj(ctx, new_xobj);
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+	}
+	else
+	{
+		copy_resource(ctx, p, PDF_NAME(XObject), name);
+		if (p->chain->op_Do_form)
+			p->chain->op_Do_form(ctx, p->chain, name, xobj, page_resources);
+	}
 }
 
 /* marked content */
@@ -1860,33 +1920,18 @@ pdf_drop_filter_processor(fz_context *ctx, pdf_processor *proc)
 	the new one as they are used. At the end therefore, this
 	contains exactly those resource objects actually required.
 
+	The filter options struct allows you to filter objects using callbacks.
 */
 pdf_processor *
-pdf_new_filter_processor(fz_context *ctx, pdf_document *doc, pdf_processor *chain, pdf_obj *old_rdb, pdf_obj *new_rdb)
-{
-	return pdf_new_filter_processor_with_text_filter(ctx, doc, -1, chain, old_rdb, new_rdb, NULL, NULL, NULL);
-}
-
-/*
-	Create a filter
-	processor with a filter function for text. This filters the
-	PDF operators it is fed, and passes them down (with some
-	changes) to the child filter.
-
-	See pdf_new_filter_processor for documentation.
-
-	text_filter: A function called to assess whether a given
-	character should be removed or not.
-
-	after_text_object: A function to be called after each text object.
-	This allows the caller to insert some extra content if
-	required.
-
-	text_filter_opaque: Opaque value to be passed to the
-	text_filter function.
-*/
-pdf_processor *
-pdf_new_filter_processor_with_text_filter(fz_context *ctx, pdf_document *doc, int structparents, pdf_processor *chain, pdf_obj *old_rdb, pdf_obj *new_rdb, pdf_text_filter_fn *text_filter, pdf_after_text_object_fn *after, void *text_filter_opaque)
+pdf_new_filter_processor(
+	fz_context *ctx,
+	pdf_document *doc,
+	pdf_processor *chain,
+	pdf_obj *old_rdb,
+	pdf_obj *new_rdb,
+	int structparents,
+	fz_matrix transform,
+	pdf_filter_options *filter)
 {
 	pdf_filter_processor *proc = pdf_new_processor(ctx, sizeof *proc);
 	{
@@ -2019,10 +2064,10 @@ pdf_new_filter_processor_with_text_filter(fz_context *ctx, pdf_document *doc, in
 	proc->chain = chain;
 	proc->old_rdb = old_rdb;
 	proc->new_rdb = new_rdb;
-
-	proc->text_filter = text_filter;
-	proc->after_text = after;
-	proc->opaque = text_filter_opaque;
+	proc->filter = filter;
+	proc->transform = transform;
+	proc->form_id = 1;
+	proc->image_id = 1;
 
 	fz_try(ctx)
 	{
diff --git a/source/pdf/pdf-write.c b/source/pdf/pdf-write.c
index 9eb450c..6b2d818 100644
--- a/source/pdf/pdf-write.c
+++ b/source/pdf/pdf-write.c
@@ -2880,6 +2880,12 @@ static void clean_content_streams(fz_context *ctx, pdf_document *doc, int saniti
 	int n = pdf_count_pages(ctx, doc);
 	int i;
 
+	pdf_filter_options filter;
+	memset(&filter, 0, sizeof filter);
+	filter.recurse = 1;
+	filter.sanitize = sanitize;
+	filter.ascii = ascii;
+
 	for (i = 0; i < n; i++)
 	{
 		pdf_annot *annot;
@@ -2887,11 +2893,10 @@ static void clean_content_streams(fz_context *ctx, pdf_document *doc, int saniti
 
 		fz_try(ctx)
 		{
-			pdf_clean_page_contents(ctx, doc, page, NULL, NULL, NULL, sanitize, ascii);
-
+			pdf_filter_page_contents(ctx, doc, page, &filter);
 			for (annot = pdf_first_annot(ctx, page); annot != NULL; annot = pdf_next_annot(ctx, annot))
 			{
-				pdf_clean_annot_contents(ctx, doc, annot, NULL, NULL, NULL, sanitize, ascii);
+				pdf_filter_annot_contents(ctx, doc, annot, &filter);
 			}
 		}
 		fz_always(ctx)

http://git.ghostscript.com/?p=mupdf.git;a=commit;h=15439869a781bbf1fe99ec08dd33af15daf67e4a

--
MuPDF library
Artifex Software, Inc.