diff mupdf-source/source/pdf/pdf-form.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children aa33339d6b8a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/source/pdf/pdf-form.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,2550 @@
+// 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 "pdf-annot-imp.h"
+
+#include <string.h>
+
+/* Must be kept in sync with definitions in pdf_util.js */
+enum
+{
+	Display_Visible,
+	Display_Hidden,
+	Display_NoPrint,
+	Display_NoView
+};
+
+enum
+{
+	SigFlag_SignaturesExist = 1,
+	SigFlag_AppendOnly = 2
+};
+
+const char *pdf_field_value(fz_context *ctx, pdf_obj *field)
+{
+	pdf_obj *v = pdf_dict_get_inheritable(ctx, field, PDF_NAME(V));
+	if (pdf_is_name(ctx, v))
+		return pdf_to_name(ctx, v);
+	if (pdf_is_stream(ctx, v))
+	{
+		// FIXME: pdf_dict_put_inheritable...
+		char *str = pdf_new_utf8_from_pdf_stream_obj(ctx, v);
+		fz_try(ctx)
+			pdf_dict_put_text_string(ctx, field, PDF_NAME(V), str);
+		fz_always(ctx)
+			fz_free(ctx, str);
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+		v = pdf_dict_get(ctx, field, PDF_NAME(V));
+	}
+	return pdf_to_text_string(ctx, v);
+}
+
+int pdf_field_flags(fz_context *ctx, pdf_obj *obj)
+{
+	return pdf_dict_get_inheritable_int(ctx, obj, PDF_NAME(Ff));
+}
+
+int pdf_field_type(fz_context *ctx, pdf_obj *obj)
+{
+	pdf_obj *type = pdf_dict_get_inheritable(ctx, obj, PDF_NAME(FT));
+	int flags = pdf_field_flags(ctx, obj);
+	if (pdf_name_eq(ctx, type, PDF_NAME(Btn)))
+	{
+		if (flags & PDF_BTN_FIELD_IS_PUSHBUTTON)
+			return PDF_WIDGET_TYPE_BUTTON;
+		else if (flags & PDF_BTN_FIELD_IS_RADIO)
+			return PDF_WIDGET_TYPE_RADIOBUTTON;
+		else
+			return PDF_WIDGET_TYPE_CHECKBOX;
+	}
+	else if (pdf_name_eq(ctx, type, PDF_NAME(Tx)))
+		return PDF_WIDGET_TYPE_TEXT;
+	else if (pdf_name_eq(ctx, type, PDF_NAME(Ch)))
+	{
+		if (flags & PDF_CH_FIELD_IS_COMBO)
+			return PDF_WIDGET_TYPE_COMBOBOX;
+		else
+			return PDF_WIDGET_TYPE_LISTBOX;
+	}
+	else if (pdf_name_eq(ctx, type, PDF_NAME(Sig)))
+		return PDF_WIDGET_TYPE_SIGNATURE;
+	else
+		return PDF_WIDGET_TYPE_BUTTON;
+}
+
+const char *pdf_field_type_string(fz_context *ctx, pdf_obj *obj)
+{
+	switch (pdf_field_type(ctx, obj))
+	{
+	default:
+	case PDF_WIDGET_TYPE_BUTTON: return "button";
+	case PDF_WIDGET_TYPE_CHECKBOX: return "checkbox";
+	case PDF_WIDGET_TYPE_COMBOBOX: return "combobox";
+	case PDF_WIDGET_TYPE_LISTBOX: return "listbox";
+	case PDF_WIDGET_TYPE_RADIOBUTTON: return "radiobutton";
+	case PDF_WIDGET_TYPE_SIGNATURE: return "signature";
+	case PDF_WIDGET_TYPE_TEXT: return "text";
+	}
+}
+
+/* Find the point in a field hierarchy where all descendants
+ * share the same name */
+static pdf_obj *find_head_of_field_group(fz_context *ctx, pdf_obj *obj)
+{
+	if (obj == NULL || pdf_dict_get(ctx, obj, PDF_NAME(T)))
+		return obj;
+	else
+		return find_head_of_field_group(ctx, pdf_dict_get(ctx, obj, PDF_NAME(Parent)));
+}
+
+static void pdf_field_mark_dirty(fz_context *ctx, pdf_obj *field)
+{
+	pdf_document *doc = pdf_get_bound_document(ctx, field);
+	pdf_obj *kids = pdf_dict_get(ctx, field, PDF_NAME(Kids));
+	if (kids)
+	{
+		int i, n = pdf_array_len(ctx, kids);
+		for (i = 0; i < n; i++)
+			pdf_field_mark_dirty(ctx, pdf_array_get(ctx, kids, i));
+	}
+	pdf_dirty_obj(ctx, field);
+	if (doc)
+		doc->resynth_required = 1;
+}
+
+static void update_field_value(fz_context *ctx, pdf_document *doc, pdf_obj *obj, const char *text)
+{
+	const char *old_text;
+	pdf_obj *grp;
+
+	if (!text)
+		text = "";
+
+	/* All fields of the same name should be updated, so
+	 * set the value at the head of the group */
+	grp = find_head_of_field_group(ctx, obj);
+	if (grp)
+		obj = grp;
+
+	/* Only update if we change the actual value. */
+	old_text = pdf_dict_get_text_string(ctx, obj, PDF_NAME(V));
+	if (old_text && !strcmp(old_text, text))
+		return;
+
+	pdf_dict_put_text_string(ctx, obj, PDF_NAME(V), text);
+
+	pdf_field_mark_dirty(ctx, obj);
+}
+
+static pdf_obj *
+pdf_lookup_field_imp(fz_context *ctx, pdf_obj *arr, const char *str, pdf_cycle_list *cycle_up);
+
+static pdf_obj *
+lookup_field_sub(fz_context *ctx, pdf_obj *dict, const char *str, pdf_cycle_list *cycle_up)
+{
+	pdf_obj *kids;
+	pdf_obj *name;
+
+	name = pdf_dict_get(ctx, dict, PDF_NAME(T));
+
+	/* If we have a name, check it matches. If it matches, consume that
+	 * portion of str. If not, exit. */
+	if (name)
+	{
+		const char *match = pdf_to_text_string(ctx, name);
+		const char *e = str;
+		size_t len;
+		while (*e && *e != '.')
+			e++;
+		len = e-str;
+		if (strncmp(str, match, len) != 0 || (match[len] != 0 && match[len] != '.'))
+			/* name doesn't match. */
+			return NULL;
+		str = e;
+		if (*str == '.')
+			str++;
+	}
+
+	/* If there is a kids array, but the search string is not empty, we have
+	encountered an internal field which represents a set of terminal fields. */
+
+	/* If there is a kids array and the search string is not empty,
+	walk those looking for the appropriate one. */
+	kids = pdf_dict_get(ctx, dict, PDF_NAME(Kids));
+	if (kids && *str != 0)
+		return pdf_lookup_field_imp(ctx, kids, str, cycle_up);
+
+	/* The field may be a terminal or an internal field at this point.
+	Accept it as the match if the match string is exhausted. */
+	if (*str == 0)
+		return dict;
+
+	return NULL;
+}
+
+static pdf_obj *
+pdf_lookup_field_imp(fz_context *ctx, pdf_obj *arr, const char *str, pdf_cycle_list *cycle_up)
+{
+	pdf_cycle_list cycle;
+	int len = pdf_array_len(ctx, arr);
+	int i;
+
+	for (i = 0; i < len; i++)
+	{
+		pdf_obj *k = pdf_array_get(ctx, arr, i);
+		pdf_obj *found;
+		if (pdf_cycle(ctx, &cycle, cycle_up, k))
+			fz_throw(ctx, FZ_ERROR_FORMAT, "cycle in fields");
+		found = lookup_field_sub(ctx, k, str, &cycle);
+		if (found)
+			return found;
+	}
+
+	return NULL;
+}
+
+pdf_obj *
+pdf_lookup_field(fz_context *ctx, pdf_obj *arr, const char *str)
+{
+	return pdf_lookup_field_imp(ctx, arr, str, NULL);
+}
+
+static void reset_form_field(fz_context *ctx, pdf_document *doc, pdf_obj *field)
+{
+	/* Set V to DV wherever DV is present, and delete V where DV is not.
+	 * FIXME: we assume for now that V has not been set unequal
+	 * to DV higher in the hierarchy than "field".
+	 *
+	 * At the bottom of the hierarchy we may find widget annotations
+	 * that aren't also fields, but DV and V will not be present in their
+	 * dictionaries, and attempts to remove V will be harmless. */
+	pdf_obj *dv = pdf_dict_get(ctx, field, PDF_NAME(DV));
+	pdf_obj *kids = pdf_dict_get(ctx, field, PDF_NAME(Kids));
+
+	if (dv)
+		pdf_dict_put(ctx, field, PDF_NAME(V), dv);
+	else
+		pdf_dict_del(ctx, field, PDF_NAME(V));
+
+	if (kids == NULL)
+	{
+		/* The leaves of the tree are widget annotations
+		 * In some cases we need to update the appearance state;
+		 * in others we need to mark the field as dirty so that
+		 * the appearance stream will be regenerated. */
+		switch (pdf_field_type(ctx, field))
+		{
+		case PDF_WIDGET_TYPE_CHECKBOX:
+		case PDF_WIDGET_TYPE_RADIOBUTTON:
+			{
+				pdf_obj *leafv = pdf_dict_get_inheritable(ctx, field, PDF_NAME(V));
+				pdf_obj *ap = pdf_dict_get(ctx, field, PDF_NAME(AP));
+				pdf_obj *n = pdf_dict_get(ctx, ap, PDF_NAME(N));
+
+				/* Value does not refer to any appearance state in the
+				normal appearance stream dictionary, default to Off instead. */
+				if (pdf_is_dict(ctx, n) && !pdf_dict_get(ctx, n, leafv))
+					leafv = NULL;
+
+				if (!pdf_is_name(ctx, leafv))
+					leafv = PDF_NAME(Off);
+				pdf_dict_put(ctx, field, PDF_NAME(AS), leafv);
+			}
+			pdf_field_mark_dirty(ctx, field);
+			break;
+
+		case PDF_WIDGET_TYPE_BUTTON:
+		case PDF_WIDGET_TYPE_SIGNATURE:
+			/* Pushbuttons and signatures have no value to reset. */
+			break;
+
+		default:
+			pdf_field_mark_dirty(ctx, field);
+			break;
+		}
+	}
+}
+
+void pdf_field_reset(fz_context *ctx, pdf_document *doc, pdf_obj *field)
+{
+	pdf_obj *kids = pdf_dict_get(ctx, field, PDF_NAME(Kids));
+
+	reset_form_field(ctx, doc, field);
+
+	if (kids)
+	{
+		int i, n = pdf_array_len(ctx, kids);
+
+		for (i = 0; i < n; i++)
+			pdf_field_reset(ctx, doc, pdf_array_get(ctx, kids, i));
+	}
+}
+
+static void add_field_hierarchy_to_array(fz_context *ctx, pdf_obj *array, pdf_obj *field, pdf_obj *fields, int exclude)
+{
+	pdf_obj *kids = pdf_dict_get(ctx, field, PDF_NAME(Kids));
+	char *needle = pdf_load_field_name(ctx, field);
+	int i, n;
+
+	fz_try(ctx)
+	{
+		n = pdf_array_len(ctx, fields);
+		for (i = 0; i < n; i++)
+		{
+			char *name = pdf_load_field_name(ctx, pdf_array_get(ctx, fields, i));
+			int found = !strcmp(needle, name);
+			fz_free(ctx, name);
+			if (found)
+				break;
+		}
+	}
+	fz_always(ctx)
+		fz_free(ctx, needle);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	if ((exclude && i < n) || (!exclude && i == n))
+		return;
+
+	pdf_array_push(ctx, array, field);
+
+	if (kids)
+	{
+		n = pdf_array_len(ctx, kids);
+
+		for (i = 0; i < n; i++)
+			add_field_hierarchy_to_array(ctx, array, pdf_array_get(ctx, kids, i), fields, exclude);
+	}
+}
+
+/*
+	When resetting or submitting a form, the fields to act upon are defined
+	by an array of either field references or field names, plus a flag determining
+	whether to act upon the fields in the array, or all fields other than those in
+	the array. specified_fields interprets this information and produces the array
+	of fields to be acted upon.
+*/
+static pdf_obj *specified_fields(fz_context *ctx, pdf_document *doc, pdf_obj *fields, int exclude)
+{
+	pdf_obj *form = pdf_dict_getl(ctx, pdf_trailer(ctx, doc), PDF_NAME(Root), PDF_NAME(AcroForm), PDF_NAME(Fields), NULL);
+	int i, n;
+	pdf_obj *result = pdf_new_array(ctx, doc, 0);
+
+	fz_try(ctx)
+	{
+		n = pdf_array_len(ctx, fields);
+
+		for (i = 0; i < n; i++)
+		{
+			pdf_obj *field = pdf_array_get(ctx, fields, i);
+
+			if (pdf_is_string(ctx, field))
+				field = pdf_lookup_field(ctx, form, pdf_to_str_buf(ctx, field));
+
+			if (field)
+				add_field_hierarchy_to_array(ctx, result, field, fields, exclude);
+		}
+	}
+	fz_catch(ctx)
+	{
+		pdf_drop_obj(ctx, result);
+		fz_rethrow(ctx);
+	}
+
+	return result;
+}
+
+void pdf_reset_form(fz_context *ctx, pdf_document *doc, pdf_obj *fields, int exclude)
+{
+	pdf_obj *sfields = specified_fields(ctx, doc, fields, exclude);
+	fz_try(ctx)
+	{
+		int i, n = pdf_array_len(ctx, sfields);
+		for (i = 0; i < n; i++)
+			reset_form_field(ctx, doc, pdf_array_get(ctx, sfields, i));
+		doc->recalculate = 1;
+	}
+	fz_always(ctx)
+		pdf_drop_obj(ctx, sfields);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+typedef struct
+{
+	pdf_obj *pageobj;
+	pdf_obj *chk;
+} lookup_state;
+
+static void *find_widget_on_page(fz_context *ctx, fz_page *page_, void *state_)
+{
+	lookup_state *state = (lookup_state *) state_;
+	pdf_page *page = (pdf_page *) page_;
+	pdf_annot *widget;
+
+	if (state->pageobj && pdf_objcmp_resolve(ctx, state->pageobj, page->obj))
+		return NULL;
+
+	for (widget = pdf_first_widget(ctx, page); widget != NULL; widget = pdf_next_widget(ctx, widget))
+	{
+		if (!pdf_objcmp_resolve(ctx, state->chk, widget->obj))
+			return widget;
+	}
+
+	return NULL;
+}
+
+static pdf_annot *find_widget(fz_context *ctx, pdf_document *doc, pdf_obj *chk)
+{
+	lookup_state state;
+
+	state.pageobj = pdf_dict_get(ctx, chk, PDF_NAME(P));
+	state.chk = chk;
+
+	return fz_process_opened_pages(ctx, (fz_document *) doc, find_widget_on_page, &state);
+}
+
+static void set_check(fz_context *ctx, pdf_document *doc, pdf_obj *chk, pdf_obj *name)
+{
+	pdf_obj *n = pdf_dict_getp(ctx, chk, "AP/N");
+	pdf_obj *val;
+
+	/* If name is a possible value of this check
+	* box then use it, otherwise use "Off" */
+	if (pdf_dict_get(ctx, n, name))
+		val = name;
+	else
+		val = PDF_NAME(Off);
+
+	if (pdf_name_eq(ctx, pdf_dict_get(ctx, chk, PDF_NAME(AS)), val))
+		return;
+
+	pdf_dict_put(ctx, chk, PDF_NAME(AS), val);
+	pdf_set_annot_has_changed(ctx, find_widget(ctx, doc, chk));
+}
+
+/* Set the values of all fields in a group defined by a node
+ * in the hierarchy */
+static void set_check_grp(fz_context *ctx, pdf_document *doc, pdf_obj *grp, pdf_obj *val)
+{
+	pdf_obj *kids = pdf_dict_get(ctx, grp, PDF_NAME(Kids));
+
+	if (kids == NULL)
+	{
+		set_check(ctx, doc, grp, val);
+	}
+	else
+	{
+		int i, n = pdf_array_len(ctx, kids);
+
+		for (i = 0; i < n; i++)
+			set_check_grp(ctx, doc, pdf_array_get(ctx, kids, i), val);
+	}
+}
+
+void pdf_calculate_form(fz_context *ctx, pdf_document *doc)
+{
+	if (doc->js)
+	{
+		fz_try(ctx)
+		{
+			pdf_obj *co = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/CO");
+			int i, n = pdf_array_len(ctx, co);
+			for (i = 0; i < n; i++)
+			{
+				pdf_obj *field = pdf_array_get(ctx, co, i);
+				pdf_field_event_calculate(ctx, doc, field);
+			}
+		}
+		fz_always(ctx)
+			doc->recalculate = 0;
+		fz_catch(ctx)
+			fz_rethrow(ctx);
+	}
+}
+
+static pdf_obj *find_on_state(fz_context *ctx, pdf_obj *dict)
+{
+	int i, n = pdf_dict_len(ctx, dict);
+	for (i = 0; i < n; ++i)
+	{
+		pdf_obj *key = pdf_dict_get_key(ctx, dict, i);
+		if (key != PDF_NAME(Off))
+			return key;
+	}
+	return NULL;
+}
+
+pdf_obj *pdf_button_field_on_state(fz_context *ctx, pdf_obj *field)
+{
+	pdf_obj *ap = pdf_dict_get(ctx, field, PDF_NAME(AP));
+	pdf_obj *on = find_on_state(ctx, pdf_dict_get(ctx, ap, PDF_NAME(N)));
+	if (!on) on = find_on_state(ctx, pdf_dict_get(ctx, ap, PDF_NAME(D)));
+	if (!on) on = PDF_NAME(Yes);
+	return on;
+}
+
+static void
+begin_annot_op(fz_context *ctx, pdf_annot *annot, const char *op)
+{
+	if (!annot->page)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "annotation not bound to any page");
+
+	pdf_begin_operation(ctx, annot->page->doc, op);
+}
+
+static void
+end_annot_op(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_end_operation(ctx, annot->page->doc);
+}
+
+static void
+abandon_annot_op(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_abandon_operation(ctx, annot->page->doc);
+}
+
+static void toggle_check_box(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_document *doc = annot->page->doc;
+
+	begin_annot_op(ctx, annot, "Toggle checkbox");
+
+	fz_try(ctx)
+	{
+		pdf_obj *field = annot->obj;
+		int ff = pdf_field_flags(ctx, field);
+		int is_radio = (ff & PDF_BTN_FIELD_IS_RADIO);
+		int is_no_toggle_to_off = (ff & PDF_BTN_FIELD_IS_NO_TOGGLE_TO_OFF);
+		pdf_obj *grp, *as, *val;
+
+		grp = find_head_of_field_group(ctx, field);
+		if (!grp)
+			grp = field;
+
+		/* TODO: check V value as well as or instead of AS? */
+		as = pdf_dict_get(ctx, field, PDF_NAME(AS));
+		if (pdf_is_name(ctx, as) && !pdf_name_eq(ctx, as, PDF_NAME(Off)))
+		{
+			if (is_radio && is_no_toggle_to_off)
+			{
+				end_annot_op(ctx, annot);
+				break;
+			}
+			val = PDF_NAME(Off);
+		}
+		else
+		{
+			val = pdf_button_field_on_state(ctx, field);
+		}
+
+		pdf_dict_put(ctx, grp, PDF_NAME(V), val);
+		set_check_grp(ctx, doc, grp, val);
+		doc->recalculate = 1;
+		end_annot_op(ctx, annot);
+	}
+	fz_catch(ctx)
+	{
+		abandon_annot_op(ctx, annot);
+		fz_rethrow(ctx);
+	}
+
+	pdf_set_annot_has_changed(ctx, annot);
+}
+
+int pdf_has_unsaved_changes(fz_context *ctx, pdf_document *doc)
+{
+	int i;
+
+	if (doc->num_incremental_sections == 0)
+		return 0;
+
+	for (i = 0; i < doc->xref_sections->num_objects; i++)
+		if (doc->xref_sections->subsec->table[i].type != 0)
+			break;
+	return i != doc->xref_sections->num_objects;
+}
+
+int pdf_was_repaired(fz_context *ctx, pdf_document *doc)
+{
+	return doc->repair_attempted;
+}
+
+int pdf_toggle_widget(fz_context *ctx, pdf_annot *widget)
+{
+	switch (pdf_widget_type(ctx, widget))
+	{
+	default:
+		return 0;
+	case PDF_WIDGET_TYPE_CHECKBOX:
+	case PDF_WIDGET_TYPE_RADIOBUTTON:
+		toggle_check_box(ctx, widget);
+		return 1;
+	}
+}
+
+int
+pdf_update_page(fz_context *ctx, pdf_page *page)
+{
+	pdf_annot *annot;
+	pdf_annot *widget;
+	int changed = 0;
+
+	fz_try(ctx)
+	{
+		pdf_begin_implicit_operation(ctx, page->doc);
+		if (page->doc->recalculate)
+			pdf_calculate_form(ctx, page->doc);
+
+		for (annot = page->annots; annot; annot = annot->next)
+			if (pdf_update_annot(ctx, annot))
+				changed = 1;
+		for (widget = page->widgets; widget; widget = widget->next)
+			if (pdf_update_annot(ctx, widget))
+				changed = 1;
+		pdf_end_operation(ctx, page->doc);
+	}
+	fz_catch(ctx)
+	{
+		pdf_abandon_operation(ctx, page->doc);
+		fz_rethrow(ctx);
+	}
+
+	return changed;
+}
+
+pdf_annot *pdf_first_widget(fz_context *ctx, pdf_page *page)
+{
+	return page->widgets;
+}
+
+pdf_annot *pdf_next_widget(fz_context *ctx, pdf_annot *widget)
+{
+	return widget->next;
+}
+
+enum pdf_widget_type pdf_widget_type(fz_context *ctx, pdf_annot *widget)
+{
+	enum pdf_widget_type ret = PDF_WIDGET_TYPE_BUTTON;
+
+	pdf_annot_push_local_xref(ctx, widget);
+
+	fz_try(ctx)
+	{
+		pdf_obj *subtype = pdf_dict_get(ctx, widget->obj, PDF_NAME(Subtype));
+		if (pdf_name_eq(ctx, subtype, PDF_NAME(Widget)))
+			ret = pdf_field_type(ctx, widget->obj);
+	}
+	fz_always(ctx)
+		pdf_annot_pop_local_xref(ctx, widget);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return ret;
+}
+
+static int set_validated_field_value(fz_context *ctx, pdf_document *doc, pdf_obj *field, const char *text, int ignore_trigger_events)
+{
+	char *newtext = NULL;
+
+	if (!ignore_trigger_events)
+	{
+		if (!pdf_field_event_validate(ctx, doc, field, text, &newtext))
+			return 0;
+	}
+
+	update_field_value(ctx, doc, field, newtext ? newtext : text);
+
+	fz_free(ctx, newtext);
+
+	return 1;
+}
+
+static void update_checkbox_selector(fz_context *ctx, pdf_document *doc, pdf_obj *field, const char *val)
+{
+	pdf_obj *kids = pdf_dict_get(ctx, field, PDF_NAME(Kids));
+
+	if (kids)
+	{
+		int i, n = pdf_array_len(ctx, kids);
+
+		for (i = 0; i < n; i++)
+			update_checkbox_selector(ctx, doc, pdf_array_get(ctx, kids, i), val);
+	}
+	else
+	{
+		pdf_obj *n = pdf_dict_getp(ctx, field, "AP/N");
+		pdf_obj *oval;
+
+		if (pdf_dict_gets(ctx, n, val))
+			oval = pdf_new_name(ctx, val);
+		else
+			oval = PDF_NAME(Off);
+		pdf_dict_put_drop(ctx, field, PDF_NAME(AS), oval);
+	}
+}
+
+static int set_checkbox_value(fz_context *ctx, pdf_document *doc, pdf_obj *field, const char *val)
+{
+	update_checkbox_selector(ctx, doc, field, val);
+	update_field_value(ctx, doc, field, val);
+	return 1;
+}
+
+int pdf_set_field_value(fz_context *ctx, pdf_document *doc, pdf_obj *field, const char *text, int ignore_trigger_events)
+{
+	int accepted = 0;
+
+	switch (pdf_field_type(ctx, field))
+	{
+	case PDF_WIDGET_TYPE_TEXT:
+	case PDF_WIDGET_TYPE_COMBOBOX:
+	case PDF_WIDGET_TYPE_LISTBOX:
+		accepted = set_validated_field_value(ctx, doc, field, text, ignore_trigger_events);
+		break;
+
+	case PDF_WIDGET_TYPE_CHECKBOX:
+	case PDF_WIDGET_TYPE_RADIOBUTTON:
+		accepted = set_checkbox_value(ctx, doc, field, text);
+		break;
+
+	default:
+		update_field_value(ctx, doc, field, text);
+		accepted = 1;
+		break;
+	}
+
+	if (!ignore_trigger_events)
+		doc->recalculate = 1;
+
+	return accepted;
+}
+
+char *pdf_field_border_style(fz_context *ctx, pdf_obj *field)
+{
+	const char *bs = pdf_to_name(ctx, pdf_dict_getl(ctx, field, PDF_NAME(BS), PDF_NAME(S), NULL));
+	switch (*bs)
+	{
+	case 'S': return "Solid";
+	case 'D': return "Dashed";
+	case 'B': return "Beveled";
+	case 'I': return "Inset";
+	case 'U': return "Underline";
+	}
+	return "Solid";
+}
+
+void pdf_field_set_border_style(fz_context *ctx, pdf_obj *field, const char *text)
+{
+	pdf_obj *val;
+
+	if (!strcmp(text, "Solid"))
+		val = PDF_NAME(S);
+	else if (!strcmp(text, "Dashed"))
+		val = PDF_NAME(D);
+	else if (!strcmp(text, "Beveled"))
+		val = PDF_NAME(B);
+	else if (!strcmp(text, "Inset"))
+		val = PDF_NAME(I);
+	else if (!strcmp(text, "Underline"))
+		val = PDF_NAME(U);
+	else
+		return;
+
+	pdf_dict_putl_drop(ctx, field, val, PDF_NAME(BS), PDF_NAME(S), NULL);
+	pdf_field_mark_dirty(ctx, field);
+}
+
+void pdf_field_set_button_caption(fz_context *ctx, pdf_obj *field, const char *text)
+{
+	if (pdf_field_type(ctx, field) == PDF_WIDGET_TYPE_BUTTON)
+	{
+		pdf_obj *val = pdf_new_text_string(ctx, text);
+		pdf_dict_putl_drop(ctx, field, val, PDF_NAME(MK), PDF_NAME(CA), NULL);
+		pdf_field_mark_dirty(ctx, field);
+	}
+}
+
+int pdf_field_display(fz_context *ctx, pdf_obj *field)
+{
+	pdf_obj *kids;
+	int f, res = Display_Visible;
+
+	/* Base response on first of children. Not ideal,
+	 * but not clear how to handle children with
+	 * differing values */
+	while ((kids = pdf_dict_get(ctx, field, PDF_NAME(Kids))) != NULL)
+		field = pdf_array_get(ctx, kids, 0);
+
+	f = pdf_dict_get_int(ctx, field, PDF_NAME(F));
+
+	if (f & PDF_ANNOT_IS_HIDDEN)
+	{
+		res = Display_Hidden;
+	}
+	else if (f & PDF_ANNOT_IS_PRINT)
+	{
+		if (f & PDF_ANNOT_IS_NO_VIEW)
+			res = Display_NoView;
+	}
+	else
+	{
+		if (f & PDF_ANNOT_IS_NO_VIEW)
+			res = Display_Hidden;
+		else
+			res = Display_NoPrint;
+	}
+
+	return res;
+}
+
+/*
+ * get the field name in a char buffer that has spare room to
+ * add more characters at the end.
+ */
+static char *load_field_name(fz_context *ctx, pdf_obj *field, int spare, pdf_cycle_list *cycle_up)
+{
+	pdf_cycle_list cycle;
+	char *res = NULL;
+	pdf_obj *parent;
+	const char *lname;
+	int llen;
+
+	if (pdf_cycle(ctx, &cycle, cycle_up, field))
+		fz_throw(ctx, FZ_ERROR_FORMAT, "Cycle in field parents");
+
+	parent = pdf_dict_get(ctx, field, PDF_NAME(Parent));
+	lname = pdf_dict_get_text_string(ctx, field, PDF_NAME(T));
+	llen = (int)strlen(lname);
+
+	// Limit fields to 16K
+	if (llen > (16 << 10) || llen + spare > (16 << 10))
+		fz_throw(ctx, FZ_ERROR_LIMIT, "Field name too long");
+
+	/*
+	 * If we found a name at this point in the field hierarchy
+	 * then we'll need extra space for it and a dot
+	 */
+	if (llen)
+		spare += llen+1;
+
+	if (parent)
+	{
+		res = load_field_name(ctx, parent, spare, &cycle);
+	}
+	else
+	{
+		res = Memento_label(fz_malloc(ctx, spare+1), "form_field_name");
+		res[0] = 0;
+	}
+
+	if (llen)
+	{
+		if (res[0])
+			strcat(res, ".");
+
+		strcat(res, lname);
+	}
+
+	return res;
+}
+
+char *pdf_load_field_name(fz_context *ctx, pdf_obj *field)
+{
+	return load_field_name(ctx, field, 0, NULL);
+}
+
+void pdf_create_field_name(fz_context *ctx, pdf_document *doc, const char *prefix, char *buf, size_t len)
+{
+	pdf_obj *form = pdf_dict_getl(ctx, pdf_trailer(ctx, doc),
+		PDF_NAME(Root), PDF_NAME(AcroForm), PDF_NAME(Fields), NULL);
+	int i;
+	for (i = 0; i < 65536; ++i) {
+		fz_snprintf(buf, len, "%s%d", prefix, i);
+		if (!pdf_lookup_field(ctx, form, buf))
+			return;
+	}
+	fz_throw(ctx, FZ_ERROR_LIMIT, "Could not create unique field name.");
+}
+
+const char *pdf_field_label(fz_context *ctx, pdf_obj *field)
+{
+	pdf_obj *label = pdf_dict_get_inheritable(ctx, field, PDF_NAME(TU));
+	if (!label)
+		label = pdf_dict_get_inheritable(ctx, field, PDF_NAME(T));
+	if (label)
+		return pdf_to_text_string(ctx, label);
+	return "Unnamed";
+}
+
+void pdf_field_set_display(fz_context *ctx, pdf_obj *field, int d)
+{
+	pdf_obj *kids = pdf_dict_get(ctx, field, PDF_NAME(Kids));
+
+	if (!kids)
+	{
+		int mask = (PDF_ANNOT_IS_HIDDEN|PDF_ANNOT_IS_PRINT|PDF_ANNOT_IS_NO_VIEW);
+		int f = pdf_dict_get_int(ctx, field, PDF_NAME(F)) & ~mask;
+
+		switch (d)
+		{
+		case Display_Visible:
+			f |= PDF_ANNOT_IS_PRINT;
+			break;
+		case Display_Hidden:
+			f |= PDF_ANNOT_IS_HIDDEN;
+			break;
+		case Display_NoView:
+			f |= (PDF_ANNOT_IS_PRINT|PDF_ANNOT_IS_NO_VIEW);
+			break;
+		case Display_NoPrint:
+			break;
+		}
+
+		pdf_dict_put_int(ctx, field, PDF_NAME(F), f);
+	}
+	else
+	{
+		int i, n = pdf_array_len(ctx, kids);
+
+		for (i = 0; i < n; i++)
+			pdf_field_set_display(ctx, pdf_array_get(ctx, kids, i), d);
+	}
+}
+
+void pdf_field_set_fill_color(fz_context *ctx, pdf_obj *field, pdf_obj *col)
+{
+	/* col == NULL mean transparent, but we can simply pass it on as with
+	 * non-NULL values because pdf_dict_putp interprets a NULL value as
+	 * delete */
+	pdf_dict_putl(ctx, field, col, PDF_NAME(MK), PDF_NAME(BG), NULL);
+	pdf_field_mark_dirty(ctx, field);
+}
+
+void pdf_field_set_text_color(fz_context *ctx, pdf_obj *field, pdf_obj *col)
+{
+	char buf[100];
+	const char *font;
+	float size, color[4];
+	/* TODO? */
+	const char *da = pdf_to_str_buf(ctx, pdf_dict_get_inheritable(ctx, field, PDF_NAME(DA)));
+	int n;
+
+	pdf_parse_default_appearance(ctx, da, &font, &size, &n, color);
+
+	switch (pdf_array_len(ctx, col))
+	{
+	default:
+		n = 0;
+		color[0] = color[1] = color[2] = color[3] = 0;
+		break;
+	case 1:
+		n = 1;
+		color[0] = pdf_array_get_real(ctx, col, 0);
+		break;
+	case 3:
+		n = 3;
+		color[0] = pdf_array_get_real(ctx, col, 0);
+		color[1] = pdf_array_get_real(ctx, col, 1);
+		color[2] = pdf_array_get_real(ctx, col, 2);
+		break;
+	case 4:
+		n = 4;
+		color[0] = pdf_array_get_real(ctx, col, 0);
+		color[1] = pdf_array_get_real(ctx, col, 1);
+		color[2] = pdf_array_get_real(ctx, col, 2);
+		color[3] = pdf_array_get_real(ctx, col, 3);
+		break;
+	}
+
+	pdf_print_default_appearance(ctx, buf, sizeof buf, font, size, n, color);
+	pdf_dict_put_string(ctx, field, PDF_NAME(DA), buf, strlen(buf));
+	pdf_field_mark_dirty(ctx, field);
+}
+
+pdf_annot *
+pdf_keep_widget(fz_context *ctx, pdf_annot *widget)
+{
+	return pdf_keep_annot(ctx, widget);
+}
+
+void
+pdf_drop_widget(fz_context *ctx, pdf_annot *widget)
+{
+	pdf_drop_annot(ctx, widget);
+}
+
+void
+pdf_drop_widgets(fz_context *ctx, pdf_annot *widget)
+{
+	while (widget)
+	{
+		pdf_annot *next = widget->next;
+		pdf_drop_widget(ctx, widget);
+		widget = next;
+	}
+}
+
+pdf_annot *
+pdf_create_signature_widget(fz_context *ctx, pdf_page *page, char *name)
+{
+	fz_rect rect = { 12, 12, 12+100, 12+50 };
+	pdf_annot *annot;
+
+	pdf_begin_operation(ctx, page->doc, "Create signature");
+
+	annot = pdf_create_annot_raw(ctx, page, PDF_ANNOT_WIDGET);
+
+	fz_try(ctx)
+	{
+		pdf_obj *obj = annot->obj;
+		pdf_obj *root = pdf_dict_get(ctx, pdf_trailer(ctx, page->doc), PDF_NAME(Root));
+		pdf_obj *acroform = pdf_dict_get(ctx, root, PDF_NAME(AcroForm));
+		pdf_obj *fields, *lock;
+		if (!acroform)
+		{
+			acroform = pdf_new_dict(ctx, page->doc, 1);
+			pdf_dict_put_drop(ctx, root, PDF_NAME(AcroForm), acroform);
+		}
+		fields = pdf_dict_get(ctx, acroform, PDF_NAME(Fields));
+		if (!fields)
+		{
+			fields = pdf_new_array(ctx, page->doc, 1);
+			pdf_dict_put_drop(ctx, acroform, PDF_NAME(Fields), fields);
+		}
+		pdf_set_annot_rect(ctx, annot, rect);
+		pdf_dict_put(ctx, obj, PDF_NAME(FT), PDF_NAME(Sig));
+		pdf_dict_put_int(ctx, obj, PDF_NAME(F), PDF_ANNOT_IS_PRINT);
+		pdf_dict_put_text_string(ctx, obj, PDF_NAME(DA), "/Helv 0 Tf 0 g");
+		pdf_dict_put_text_string(ctx, obj, PDF_NAME(T), name);
+		pdf_array_push(ctx, fields, obj);
+		lock = pdf_dict_put_dict(ctx, obj, PDF_NAME(Lock), 1);
+		pdf_dict_put(ctx, lock, PDF_NAME(Action), PDF_NAME(All));
+		pdf_end_operation(ctx, page->doc);
+	}
+	fz_catch(ctx)
+	{
+		pdf_abandon_operation(ctx, page->doc);
+		pdf_delete_annot(ctx, page, annot);
+	}
+	return (pdf_annot *)annot;
+}
+
+fz_rect
+pdf_bound_widget(fz_context *ctx, pdf_annot *widget)
+{
+	return pdf_bound_annot(ctx, widget);
+}
+
+int
+pdf_update_widget(fz_context *ctx, pdf_annot *widget)
+{
+	return pdf_update_annot(ctx, widget);
+}
+
+int pdf_text_widget_max_len(fz_context *ctx, pdf_annot *tw)
+{
+	pdf_annot *annot = (pdf_annot *)tw;
+	return pdf_dict_get_inheritable_int(ctx, annot->obj, PDF_NAME(MaxLen));
+}
+
+int pdf_text_widget_format(fz_context *ctx, pdf_annot *tw)
+{
+	pdf_annot *annot = (pdf_annot *)tw;
+	int type = PDF_WIDGET_TX_FORMAT_NONE;
+	pdf_obj *js = pdf_dict_getl(ctx, annot->obj, PDF_NAME(AA), PDF_NAME(F), PDF_NAME(JS), NULL);
+	if (js)
+	{
+		char *code = pdf_load_stream_or_string_as_utf8(ctx, js);
+		if (strstr(code, "AFNumber_Format"))
+			type = PDF_WIDGET_TX_FORMAT_NUMBER;
+		else if (strstr(code, "AFSpecial_Format"))
+			type = PDF_WIDGET_TX_FORMAT_SPECIAL;
+		else if (strstr(code, "AFDate_FormatEx"))
+			type = PDF_WIDGET_TX_FORMAT_DATE;
+		else if (strstr(code, "AFTime_FormatEx"))
+			type = PDF_WIDGET_TX_FORMAT_TIME;
+		fz_free(ctx, code);
+	}
+
+	return type;
+}
+
+static char *
+merge_changes(fz_context *ctx, const char *value, int start, int end, const char *change)
+{
+	int changelen = change ? (int)strlen(change) : 0;
+	int valuelen = value ? (int)strlen(value) : 0;
+	int prelen = (start >= 0 ? (start < valuelen ? start : valuelen) : 0);
+	int postlen = (end >= 0 && end <= valuelen ? valuelen - end : 0);
+	int newlen =  prelen + changelen + postlen + 1;
+	char *merged = fz_malloc(ctx, newlen);
+	char *m = merged;
+
+	if (prelen)
+	{
+		memcpy(m, value, prelen);
+		m += prelen;
+	}
+	if (changelen)
+	{
+		memcpy(m, change, changelen);
+		m += changelen;
+	}
+	if (postlen)
+	{
+		memcpy(m, &value[end], postlen);
+		m += postlen;
+	}
+	*m = 0;
+
+	return merged;
+}
+
+int pdf_set_text_field_value(fz_context *ctx, pdf_annot *widget, const char *update)
+{
+	pdf_document *doc;
+	pdf_keystroke_event evt = { 0 };
+	char *new_change = NULL;
+	char *new_value = NULL;
+	char *merged_value = NULL;
+	int rc = 1;
+
+	begin_annot_op(ctx, widget, "Edit text field");
+	doc = widget->page->doc;
+
+	fz_var(new_value);
+	fz_var(new_change);
+	fz_var(merged_value);
+	fz_try(ctx)
+	{
+		if (!widget->ignore_trigger_events)
+		{
+			evt.value = pdf_annot_field_value(ctx, widget);
+			evt.change = update;
+			evt.selStart = 0;
+			evt.selEnd = (int)strlen(evt.value);
+			evt.willCommit = 0;
+			rc = pdf_annot_field_event_keystroke(ctx, doc, widget, &evt);
+			new_change = evt.newChange;
+			new_value = evt.newValue;
+			evt.newValue = NULL;
+			evt.newChange = NULL;
+			if (rc)
+			{
+				merged_value = merge_changes(ctx, new_value, evt.selStart, evt.selEnd, new_change);
+				evt.value = merged_value;
+				evt.change = "";
+				evt.selStart = -1;
+				evt.selEnd = -1;
+				evt.willCommit = 1;
+				rc = pdf_annot_field_event_keystroke(ctx, doc, widget, &evt);
+				if (rc)
+					rc = pdf_set_annot_field_value(ctx, doc, widget, evt.newValue, 0);
+			}
+		}
+		else
+		{
+			rc = pdf_set_annot_field_value(ctx, doc, widget, update, 1);
+		}
+		end_annot_op(ctx, widget);
+	}
+	fz_always(ctx)
+	{
+		fz_free(ctx, new_value);
+		fz_free(ctx, evt.newValue);
+		fz_free(ctx, new_change);
+		fz_free(ctx, evt.newChange);
+		fz_free(ctx, merged_value);
+	}
+	fz_catch(ctx)
+	{
+		abandon_annot_op(ctx, widget);
+		fz_warn(ctx, "could not set widget text");
+		rc = 0;
+	}
+	return rc;
+}
+
+int pdf_edit_text_field_value(fz_context *ctx, pdf_annot *widget, const char *value, const char *change, int *selStart, int *selEnd, char **result)
+{
+	pdf_document *doc = widget->page->doc;
+	pdf_keystroke_event evt = {0};
+	int rc = 1;
+
+	pdf_begin_operation(ctx, doc, "Text field keystroke");
+
+	fz_try(ctx)
+	{
+		if (!widget->ignore_trigger_events)
+		{
+			evt.value = value;
+			evt.change = change;
+			evt.selStart = *selStart;
+			evt.selEnd = *selEnd;
+			evt.willCommit = 0;
+			rc = pdf_annot_field_event_keystroke(ctx, doc, widget, &evt);
+			if (rc)
+			{
+				*result = merge_changes(ctx, evt.newValue, evt.selStart, evt.selEnd, evt.newChange);
+				*selStart = evt.selStart + (int)strlen(evt.newChange);
+				*selEnd = *selStart;
+			}
+		}
+		else
+		{
+			*result = merge_changes(ctx, value, *selStart, *selEnd, change);
+			*selStart = evt.selStart + (int)strlen(change);
+			*selEnd = *selStart;
+		}
+		pdf_end_operation(ctx, doc);
+	}
+	fz_always(ctx)
+	{
+		fz_free(ctx, evt.newValue);
+		fz_free(ctx, evt.newChange);
+	}
+	fz_catch(ctx)
+	{
+		pdf_abandon_operation(ctx, doc);
+		fz_warn(ctx, "could not process text widget keystroke");
+		rc = 0;
+	}
+	return rc;
+}
+
+int pdf_set_choice_field_value(fz_context *ctx, pdf_annot *widget, const char *new_value)
+{
+	/* Choice widgets use almost the same keystroke processing as text fields. */
+	return pdf_set_text_field_value(ctx, widget, new_value);
+}
+
+int pdf_choice_widget_options(fz_context *ctx, pdf_annot *tw, int exportval, const char *opts[])
+{
+	pdf_annot *annot = (pdf_annot *)tw;
+	pdf_obj *optarr;
+	int i, n, m;
+
+	optarr = pdf_dict_get_inheritable(ctx, annot->obj, PDF_NAME(Opt));
+	n = pdf_array_len(ctx, optarr);
+
+	if (opts)
+	{
+		for (i = 0; i < n; i++)
+		{
+			m = pdf_array_len(ctx, pdf_array_get(ctx, optarr, i));
+			/* If it is a two element array, the second item is the one that we want if we want the listing value. */
+			if (m == 2)
+				if (exportval)
+					opts[i] = pdf_array_get_text_string(ctx, pdf_array_get(ctx, optarr, i), 0);
+				else
+					opts[i] = pdf_array_get_text_string(ctx, pdf_array_get(ctx, optarr, i), 1);
+			else
+				opts[i] = pdf_array_get_text_string(ctx, optarr, i);
+		}
+	}
+	return n;
+}
+
+int pdf_choice_field_option_count(fz_context *ctx, pdf_obj *field)
+{
+	pdf_obj *opt = pdf_dict_get_inheritable(ctx, field, PDF_NAME(Opt));
+	return pdf_array_len(ctx, opt);
+}
+
+const char *pdf_choice_field_option(fz_context *ctx, pdf_obj *field, int export, int i)
+{
+	pdf_obj *opt = pdf_dict_get_inheritable(ctx, field, PDF_NAME(Opt));
+	pdf_obj *ent = pdf_array_get(ctx, opt, i);
+	if (pdf_array_len(ctx, ent) == 2)
+		return pdf_array_get_text_string(ctx, ent, export ? 0 : 1);
+	else
+		return pdf_to_text_string(ctx, ent);
+}
+
+int pdf_choice_widget_is_multiselect(fz_context *ctx, pdf_annot *tw)
+{
+	pdf_annot *annot = (pdf_annot *)tw;
+
+	if (!annot) return 0;
+
+	switch (pdf_field_type(ctx, annot->obj))
+	{
+	case PDF_WIDGET_TYPE_LISTBOX:
+		return (pdf_field_flags(ctx, annot->obj) & PDF_CH_FIELD_IS_MULTI_SELECT) != 0;
+	default:
+		return 0;
+	}
+}
+
+int pdf_choice_widget_value(fz_context *ctx, pdf_annot *tw, const char *opts[])
+{
+	pdf_annot *annot = (pdf_annot *)tw;
+	pdf_obj *optarr;
+	int i, n;
+
+	if (!annot)
+		return 0;
+
+	optarr = pdf_dict_get(ctx, annot->obj, PDF_NAME(V));
+
+	if (pdf_is_string(ctx, optarr))
+	{
+		if (opts)
+			opts[0] = pdf_to_text_string(ctx, optarr);
+		return 1;
+	}
+	else
+	{
+		n = pdf_array_len(ctx, optarr);
+		if (opts)
+		{
+			for (i = 0; i < n; i++)
+			{
+				pdf_obj *elem = pdf_array_get(ctx, optarr, i);
+				if (pdf_is_array(ctx, elem))
+					elem = pdf_array_get(ctx, elem, 1);
+				opts[i] = pdf_to_text_string(ctx, elem);
+			}
+		}
+		return n;
+	}
+}
+
+void pdf_choice_widget_set_value(fz_context *ctx, pdf_annot *tw, int n, const char *opts[])
+{
+	pdf_annot *annot = (pdf_annot *)tw;
+	pdf_obj *optarr = NULL;
+	int i;
+
+	if (!annot)
+		return;
+
+	begin_annot_op(ctx, annot, "Set choice");
+
+	fz_var(optarr);
+	fz_try(ctx)
+	{
+		if (n != 1)
+		{
+			optarr = pdf_new_array(ctx, annot->page->doc, n);
+
+			for (i = 0; i < n; i++)
+				pdf_array_push_text_string(ctx, optarr, opts[i]);
+
+			pdf_dict_put_drop(ctx, annot->obj, PDF_NAME(V), optarr);
+		}
+		else
+			pdf_dict_put_text_string(ctx, annot->obj, PDF_NAME(V), opts[0]);
+
+		/* FIXME: when n > 1, we should be regenerating the indexes */
+		pdf_dict_del(ctx, annot->obj, PDF_NAME(I));
+
+		pdf_field_mark_dirty(ctx, annot->obj);
+		end_annot_op(ctx, annot);
+	}
+	fz_catch(ctx)
+	{
+		abandon_annot_op(ctx, annot);
+		pdf_drop_obj(ctx, optarr);
+		fz_rethrow(ctx);
+	}
+}
+
+int pdf_signature_byte_range(fz_context *ctx, pdf_document *doc, pdf_obj *signature, fz_range *byte_range)
+{
+	pdf_obj *br = pdf_dict_getl(ctx, signature, PDF_NAME(V), PDF_NAME(ByteRange), NULL);
+	int i, n = pdf_array_len(ctx, br)/2;
+
+	if (byte_range)
+	{
+		for (i = 0; i < n; i++)
+		{
+			int64_t offset = pdf_array_get_int(ctx, br, 2*i);
+			int length = pdf_array_get_int(ctx, br, 2*i+1);
+
+			if (offset < 0 || offset > doc->file_size)
+				fz_throw(ctx, FZ_ERROR_FORMAT, "offset of signature byte range outside of file");
+			else if (length < 0)
+				fz_throw(ctx, FZ_ERROR_FORMAT, "length of signature byte range negative");
+			else if (offset + length > doc->file_size)
+				fz_throw(ctx, FZ_ERROR_FORMAT, "signature byte range extends past end of file");
+
+			byte_range[i].offset = offset;
+			byte_range[i].length = length;
+		}
+	}
+
+	return n;
+}
+
+static int is_white(int c)
+{
+	return c == '\x00' || c == '\x09' || c == '\x0a' || c == '\x0c' || c == '\x0d' || c == '\x20';
+}
+
+static int is_hex_or_white(int c)
+{
+	return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9') || is_white(c);
+}
+
+static void validate_certificate_data(fz_context *ctx, pdf_document *doc, fz_range *hole)
+{
+	fz_stream *stm;
+	int c;
+
+	stm = fz_open_range_filter(ctx, doc->file, hole, 1);
+	fz_try(ctx)
+	{
+		while (is_white((c = fz_read_byte(ctx, stm))))
+			;
+
+		if (c == '<')
+			c = fz_read_byte(ctx, stm);
+
+		while (is_hex_or_white(c))
+			c = fz_read_byte(ctx, stm);
+
+		if (c == '>')
+			c = fz_read_byte(ctx, stm);
+
+		while (is_white(c))
+			c = fz_read_byte(ctx, stm);
+
+		if (c != EOF)
+			fz_throw(ctx, FZ_ERROR_FORMAT, "signature certificate data contains invalid character");
+		if ((size_t)fz_tell(ctx, stm) != hole->length)
+			fz_throw(ctx, FZ_ERROR_FORMAT, "premature end of signature certificate data");
+	}
+	fz_always(ctx)
+		fz_drop_stream(ctx, stm);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static int rangecmp(const void *a_, const void *b_)
+{
+	const fz_range *a = (const fz_range *) a_;
+	const fz_range *b = (const fz_range *) b_;
+	return (int) (a->offset - b->offset);
+}
+
+static void validate_byte_ranges(fz_context *ctx, pdf_document *doc, fz_range *unsorted, int nranges)
+{
+	int64_t offset = 0;
+	fz_range *sorted;
+	int i;
+
+	sorted = fz_calloc(ctx, nranges, sizeof(*sorted));
+	memcpy(sorted, unsorted, nranges * sizeof(*sorted));
+	qsort(sorted, nranges, sizeof(*sorted), rangecmp);
+
+	fz_try(ctx)
+	{
+		offset = 0;
+
+		for (i = 0; i < nranges; i++)
+		{
+			if (sorted[i].offset > offset)
+			{
+				fz_range hole;
+
+				hole.offset = offset;
+				hole.length = sorted[i].offset - offset;
+
+				validate_certificate_data(ctx, doc, &hole);
+			}
+
+			offset = fz_maxi64(offset, sorted[i].offset + sorted[i].length);
+		}
+	}
+	fz_always(ctx)
+		fz_free(ctx, sorted);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+fz_stream *pdf_signature_hash_bytes(fz_context *ctx, pdf_document *doc, pdf_obj *signature)
+{
+	fz_range *byte_range = NULL;
+	int byte_range_len;
+	fz_stream *bytes = NULL;
+
+	fz_var(byte_range);
+	fz_try(ctx)
+	{
+		byte_range_len = pdf_signature_byte_range(ctx, doc, signature, NULL);
+		if (byte_range_len)
+		{
+			byte_range = fz_calloc(ctx, byte_range_len, sizeof(*byte_range));
+			pdf_signature_byte_range(ctx, doc, signature, byte_range);
+		}
+
+		validate_byte_ranges(ctx, doc, byte_range, byte_range_len);
+		bytes = fz_open_range_filter(ctx, doc->file, byte_range, byte_range_len);
+	}
+	fz_always(ctx)
+	{
+		fz_free(ctx, byte_range);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow(ctx);
+	}
+
+	return bytes;
+}
+
+int pdf_incremental_change_since_signing_widget(fz_context *ctx, pdf_annot *widget)
+{
+	if (!widget->page)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "annotation not bound to any page");
+	return pdf_signature_incremental_change_since_signing(ctx, widget->page->doc, widget->obj);
+}
+
+int pdf_signature_incremental_change_since_signing(fz_context *ctx, pdf_document *doc, pdf_obj *signature)
+{
+	fz_range *byte_range = NULL;
+	int byte_range_len;
+	int changed = 0;
+
+	if (pdf_dict_get_inheritable(ctx, signature, PDF_NAME(FT)) != PDF_NAME(Sig))
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "annotation is not a signature widget");
+	if (!pdf_signature_is_signed(ctx, doc, signature))
+		return 0;
+
+	fz_var(byte_range);
+	fz_try(ctx)
+	{
+		byte_range_len = pdf_signature_byte_range(ctx, doc, signature, NULL);
+		if (byte_range_len)
+		{
+			fz_range *last_range;
+			int64_t end_of_range;
+
+			byte_range = fz_calloc(ctx, byte_range_len, sizeof(*byte_range));
+			pdf_signature_byte_range(ctx, doc, signature, byte_range);
+
+			last_range = &byte_range[byte_range_len -1];
+			end_of_range = last_range->offset + last_range->length;
+
+			/* We can see how long the document was when signed by inspecting the byte
+			 * ranges of the signature.  The document, when read in, may have already
+			 * had changes tagged on to it, past its extent when signed, or we may have
+			 * made changes since reading it, which will be held in a new incremental
+			 * xref section. */
+			if (doc->file_size > end_of_range || doc->num_incremental_sections > 0)
+				changed = 1;
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_free(ctx, byte_range);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow(ctx);
+	}
+
+	return changed;
+}
+
+int pdf_signature_is_signed(fz_context *ctx, pdf_document *doc, pdf_obj *field)
+{
+	pdf_obj *v;
+	pdf_obj* vtype;
+
+	if (pdf_dict_get_inheritable(ctx, field, PDF_NAME(FT)) != PDF_NAME(Sig))
+		return 0;
+	/* Signatures can only be signed if the value is a dictionary,
+	 * and if the value has a Type, it should be Sig. */
+	v = pdf_dict_get_inheritable(ctx, field, PDF_NAME(V));
+	vtype = pdf_dict_get(ctx, v, PDF_NAME(Type));
+	return pdf_is_dict(ctx, v) && (vtype ? pdf_name_eq(ctx, vtype, PDF_NAME(Sig)) : 1);
+}
+
+int pdf_widget_is_signed(fz_context *ctx, pdf_annot *widget)
+{
+	if (widget == NULL)
+		return 0;
+
+	if (!widget->page)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "annotation not bound to any page");
+
+	return pdf_signature_is_signed(ctx, widget->page->doc, widget->obj);
+}
+
+int pdf_widget_is_readonly(fz_context *ctx, pdf_annot *widget)
+{
+	int fflags;
+	if (widget == NULL)
+		return 0;
+	fflags = pdf_field_flags(ctx, ((pdf_annot *) widget)->obj);
+	return fflags & PDF_FIELD_IS_READ_ONLY;
+}
+
+size_t pdf_signature_contents(fz_context *ctx, pdf_document *doc, pdf_obj *signature, char **contents)
+{
+	pdf_obj *v_ref = pdf_dict_get_inheritable(ctx, signature, PDF_NAME(V));
+	pdf_obj *v_obj = pdf_load_unencrypted_object(ctx, doc, pdf_to_num(ctx, v_ref));
+	char *copy = NULL;
+	size_t len;
+
+	fz_var(copy);
+	fz_try(ctx)
+	{
+		pdf_obj *c = pdf_dict_get(ctx, v_obj, PDF_NAME(Contents));
+		char *s;
+
+		s = pdf_to_str_buf(ctx, c);
+		len = pdf_to_str_len(ctx, c);
+
+		if (contents)
+		{
+			copy = Memento_label(fz_malloc(ctx, len), "sig_contents");
+			memcpy(copy, s, len);
+		}
+	}
+	fz_always(ctx)
+		pdf_drop_obj(ctx, v_obj);
+	fz_catch(ctx)
+	{
+		fz_free(ctx, copy);
+		fz_rethrow(ctx);
+	}
+
+	if (contents)
+		*contents = copy;
+	return len;
+}
+
+static fz_xml_doc *load_xfa(fz_context *ctx, pdf_document *doc)
+{
+	pdf_obj *xfa;
+	fz_buffer *buf = NULL;
+	fz_buffer *packet = NULL;
+	int i;
+
+	if (doc->xfa)
+		return doc->xfa; /* Already loaded, and present. */
+
+	xfa = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/XFA");
+	if (!pdf_is_array(ctx, xfa) && !pdf_is_stream(ctx, xfa))
+		return NULL; /* No XFA */
+
+	fz_var(buf);
+	fz_var(packet);
+
+	fz_try(ctx)
+	{
+		if (pdf_is_stream(ctx, xfa))
+		{
+			/* Load entire XFA resource */
+			buf = pdf_load_stream(ctx, xfa);
+		}
+		else
+		{
+			/* Concatenate packets to create entire XFA resource */
+			buf = fz_new_buffer(ctx, 1024);
+			for(i = 0; i < pdf_array_len(ctx, xfa); ++i)
+			{
+				pdf_obj *ref = pdf_array_get(ctx, xfa, i);
+				if (pdf_is_stream(ctx, ref))
+				{
+					packet = pdf_load_stream(ctx, ref);
+					fz_append_buffer(ctx, buf, packet);
+					fz_drop_buffer(ctx, packet);
+					packet = NULL;
+				}
+			}
+		}
+
+		/* Parse and stow away XFA resource in document */
+		doc->xfa = fz_parse_xml(ctx, buf, 0);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_buffer(ctx, packet);
+		fz_drop_buffer(ctx, buf);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow(ctx);
+	}
+
+	return doc->xfa;
+}
+
+static fz_xml *
+get_xfa_resource(fz_context *ctx, pdf_document *doc, const char *str)
+{
+	fz_xml_doc *xfa;
+
+	xfa = load_xfa(ctx, doc);
+	if (!xfa)
+		return NULL;
+
+	return fz_xml_find_down(fz_xml_root(xfa), str);
+}
+
+static int
+find_name_component(char **np, char **sp, char **ep)
+{
+	char *n = *np;
+	char *s, *e;
+	int idx = 0;
+
+	if (*n == '.')
+		n++;
+
+	/* Find the next name we are looking for. */
+	s = e = n;
+	while (*e && *e != '[' && *e != '.')
+		e++;
+
+	/* So the next name is s..e */
+	n = e;
+	if (*n == '[')
+	{
+		n++;
+		while (*n >= '0' && *n <= '9')
+			idx = idx*10 + *n++ - '0';
+		while (*n && *n != ']')
+			n++;
+		if (*n == ']')
+			n++;
+	}
+	*np = n;
+	*sp = s;
+	*ep = e;
+
+	return idx;
+}
+
+static pdf_obj *
+annot_from_name(fz_context *ctx, pdf_document *doc, const char *str)
+{
+	pdf_obj *fields = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/Fields");
+
+	if (strncmp(str, "xfa[0].", 7) == 0)
+		str += 7;
+	if (strncmp(str, "template[0].", 12) == 0)
+		str += 12;
+
+	return pdf_lookup_field(ctx, fields, str);
+}
+
+static pdf_obj *
+get_locked_fields_from_xfa(fz_context *ctx, pdf_document *doc, pdf_obj *field)
+{
+	char *name = pdf_load_field_name(ctx, field);
+	char *n = name;
+	const char *use;
+	fz_xml *node;
+
+	if (name == NULL)
+		return NULL;
+
+	fz_try(ctx)
+	{
+		node = get_xfa_resource(ctx, doc, "template");
+
+		do
+		{
+			char c, *s, *e;
+			int idx = 0;
+			char *key;
+
+			idx = find_name_component(&n, &s, &e);
+			/* We want the idx'th occurrence of s..e */
+
+			/* Hacky */
+			c = *e;
+			*e = 0;
+			key = *n ? "subform" : "field";
+			node = fz_xml_find_down_match(node, key, "name", s);
+			while (node && idx > 0)
+			{
+				node = fz_xml_find_next_match(node, key, "name", s);
+				idx--;
+			}
+			*e = c;
+		}
+		while (node && *n == '.');
+	}
+	fz_always(ctx)
+		fz_free(ctx, name);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	if (node == NULL)
+		return NULL;
+
+	node = fz_xml_find_down(node, "ui");
+	node = fz_xml_find_down(node, "signature");
+	node = fz_xml_find_down(node, "manifest");
+
+	use = fz_xml_att(node, "use");
+	if (use == NULL)
+		return NULL;
+	if (*use == '#')
+		use++;
+
+	/* Now look for a variables entry in a subform that defines this. */
+	while (node)
+	{
+		fz_xml *variables, *manifest, *ref;
+		pdf_obj *arr;
+
+		/* Find the enclosing subform */
+		do {
+			node = fz_xml_up(node);
+		} while (node && strcmp(fz_xml_tag(node), "subform"));
+
+		/* Look for a variables within that. */
+		variables = fz_xml_find_down(node, "variables");
+		if (variables == NULL)
+			continue;
+
+		manifest = fz_xml_find_down_match(variables, "manifest", "id", use);
+		if (manifest == NULL)
+			continue;
+
+		arr = pdf_new_array(ctx, doc, 16);
+		fz_try(ctx)
+		{
+			ref = fz_xml_find_down(manifest, "ref");
+			while (ref)
+			{
+				const char *s = fz_xml_text(fz_xml_down(ref));
+				pdf_array_push(ctx, arr, annot_from_name(ctx, doc, s));
+				ref = fz_xml_find_next(ref, "ref");
+			}
+		}
+		fz_catch(ctx)
+		{
+			pdf_drop_obj(ctx, arr);
+			fz_rethrow(ctx);
+		}
+		return arr;
+	}
+
+	return NULL;
+}
+
+static void
+lock_field(fz_context *ctx, pdf_obj *f)
+{
+	int ff = pdf_dict_get_inheritable_int(ctx, f, PDF_NAME(Ff));
+
+	if ((ff & PDF_FIELD_IS_READ_ONLY) ||
+		!pdf_name_eq(ctx, pdf_dict_get(ctx, f, PDF_NAME(Type)), PDF_NAME(Annot)) ||
+		!pdf_name_eq(ctx, pdf_dict_get(ctx, f, PDF_NAME(Subtype)), PDF_NAME(Widget)))
+		return;
+
+	pdf_dict_put_int(ctx, f, PDF_NAME(Ff), ff | PDF_FIELD_IS_READ_ONLY);
+}
+
+static void
+lock_xfa_locked_fields(fz_context *ctx, pdf_obj *a)
+{
+	int i;
+	int len = pdf_array_len(ctx, a);
+
+	for (i = 0; i < len; i++)
+	{
+		lock_field(ctx, pdf_array_get(ctx, a, i));
+	}
+}
+
+
+void pdf_signature_set_value(fz_context *ctx, pdf_document *doc, pdf_obj *field, pdf_pkcs7_signer *signer, int64_t stime)
+{
+	pdf_obj *v = NULL;
+	pdf_obj *o = NULL;
+	pdf_obj *r = NULL;
+	pdf_obj *t = NULL;
+	pdf_obj *a = NULL;
+	pdf_obj *b = NULL;
+	pdf_obj *l = NULL;
+	pdf_obj *indv;
+	int vnum;
+	size_t max_digest_size;
+	char *buf = NULL;
+
+	vnum = pdf_create_object(ctx, doc);
+	indv = pdf_new_indirect(ctx, doc, vnum, 0);
+	pdf_dict_put_drop(ctx, field, PDF_NAME(V), indv);
+
+	max_digest_size = signer->max_digest_size(ctx, signer);
+
+	fz_var(v);
+	fz_var(o);
+	fz_var(r);
+	fz_var(t);
+	fz_var(a);
+	fz_var(b);
+	fz_var(l);
+	fz_var(buf);
+	fz_try(ctx)
+	{
+		v = pdf_new_dict(ctx, doc, 4);
+		pdf_update_object(ctx, doc, vnum, v);
+
+		buf = fz_calloc(ctx, max_digest_size, 1);
+
+		/* Ensure that the /Filter entry is the first entry in the
+		   dictionary after the digest contents since we look for
+		   this tag when completing signatures in pdf-write.c in order
+		   to generate the correct byte range. */
+		pdf_dict_put_array(ctx, v, PDF_NAME(ByteRange), 4);
+		pdf_dict_put_string(ctx, v, PDF_NAME(Contents), buf, max_digest_size);
+		pdf_dict_put(ctx, v, PDF_NAME(Filter), PDF_NAME(Adobe_PPKLite));
+		pdf_dict_put(ctx, v, PDF_NAME(SubFilter), PDF_NAME(adbe_pkcs7_detached));
+		pdf_dict_put(ctx, v, PDF_NAME(Type), PDF_NAME(Sig));
+		pdf_dict_put_date(ctx, v, PDF_NAME(M), stime);
+
+		o = pdf_dict_put_array(ctx, v, PDF_NAME(Reference), 1);
+		r = pdf_array_put_dict(ctx, o, 0, 4);
+		pdf_dict_put(ctx, r, PDF_NAME(Data), pdf_dict_get(ctx, pdf_trailer(ctx, doc), PDF_NAME(Root)));
+		pdf_dict_put(ctx, r, PDF_NAME(TransformMethod), PDF_NAME(FieldMDP));
+		pdf_dict_put(ctx, r, PDF_NAME(Type), PDF_NAME(SigRef));
+		t = pdf_dict_put_dict(ctx, r, PDF_NAME(TransformParams), 5);
+
+		l = pdf_dict_getp(ctx, field, "Lock/Action");
+		if (l)
+		{
+			a = pdf_dict_getp(ctx, field, "Lock/Fields");
+		}
+		else
+		{
+			/* Lock action wasn't specified so we need to encode an Include.
+			 * Before we just use an empty array, check in the XFA for locking
+			 * details. */
+			a = get_locked_fields_from_xfa(ctx, doc, field);
+			if (a)
+				lock_xfa_locked_fields(ctx, a);
+
+			/* If we don't get a result from the XFA, just encode an empty array
+			 * (leave a == NULL), even if Lock/Fields exists because we don't really
+			 * know what to do with the information if the action isn't defined. */
+			l = PDF_NAME(Include);
+		}
+
+		pdf_dict_put(ctx, t, PDF_NAME(Action), l);
+
+		if (pdf_name_eq(ctx, l, PDF_NAME(Include)) || pdf_name_eq(ctx, l, PDF_NAME(Exclude)))
+		{
+			/* For action Include and Exclude, we need to encode a Fields array */
+			if (!a)
+			{
+				/* If one wasn't defined or we chose to ignore it because no action
+				 * was defined then use an empty one. */
+				b = pdf_new_array(ctx, doc, 0);
+				a = b;
+			}
+
+			pdf_dict_put_drop(ctx, t, PDF_NAME(Fields), pdf_copy_array(ctx, a));
+		}
+
+		pdf_dict_put(ctx, t, PDF_NAME(Type), PDF_NAME(TransformParams));
+		pdf_dict_put(ctx, t, PDF_NAME(V), PDF_NAME(1_2));
+
+		/* Record details within the document structure so that contents
+		* and byte_range can be updated with their correct values at
+		* saving time */
+		pdf_xref_store_unsaved_signature(ctx, doc, field, signer);
+	}
+	fz_always(ctx)
+	{
+		pdf_drop_obj(ctx, v);
+		pdf_drop_obj(ctx, b);
+		fz_free(ctx, buf);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow(ctx);
+	}
+}
+
+void pdf_set_widget_editing_state(fz_context *ctx, pdf_annot *widget, int editing)
+{
+	widget->ignore_trigger_events = editing;
+}
+
+int pdf_get_widget_editing_state(fz_context *ctx, pdf_annot *widget)
+{
+	return widget->ignore_trigger_events;
+}
+
+static void pdf_execute_js_action(fz_context *ctx, pdf_document *doc, pdf_obj *target, const char *path, pdf_obj *js)
+{
+	if (js)
+	{
+		char *code = pdf_load_stream_or_string_as_utf8(ctx, js);
+		int in_op = 0;
+
+		fz_var(in_op);
+		fz_try(ctx)
+		{
+			char buf[100];
+			fz_snprintf(buf, sizeof buf, "%d/%s", pdf_to_num(ctx, target), path);
+			pdf_begin_operation(ctx, doc, "Javascript Event");
+			in_op = 1;
+			pdf_js_execute(doc->js, buf, code, NULL);
+			pdf_end_operation(ctx, doc);
+		}
+		fz_always(ctx)
+		{
+			fz_free(ctx, code);
+		}
+		fz_catch(ctx)
+		{
+			if (in_op)
+				pdf_abandon_operation(ctx, doc);
+			fz_rethrow(ctx);
+		}
+	}
+}
+
+static void pdf_execute_action_imp(fz_context *ctx, pdf_document *doc, pdf_obj *target, const char *path, pdf_obj *action)
+{
+	pdf_obj *S = pdf_dict_get(ctx, action, PDF_NAME(S));
+	if (pdf_name_eq(ctx, S, PDF_NAME(JavaScript)))
+	{
+		if (doc->js)
+			pdf_execute_js_action(ctx, doc, target, path, pdf_dict_get(ctx, action, PDF_NAME(JS)));
+	}
+	if (pdf_name_eq(ctx, S, PDF_NAME(ResetForm)))
+	{
+		pdf_obj *fields = pdf_dict_get(ctx, action, PDF_NAME(Fields));
+		int flags = pdf_dict_get_int(ctx, action, PDF_NAME(Flags));
+		pdf_reset_form(ctx, doc, fields, flags & 1);
+	}
+}
+
+static void pdf_execute_action_chain(fz_context *ctx, pdf_document *doc, pdf_obj *target, const char *path, pdf_obj *action, pdf_cycle_list *cycle_up)
+{
+	pdf_cycle_list cycle;
+	pdf_obj *next;
+
+	if (pdf_cycle(ctx, &cycle, cycle_up, action))
+		fz_throw(ctx, FZ_ERROR_FORMAT, "cycle in action chain");
+
+	if (pdf_is_array(ctx, action))
+	{
+		int i, n = pdf_array_len(ctx, action);
+		for (i = 0; i < n; ++i)
+			pdf_execute_action_chain(ctx, doc, target, path, pdf_array_get(ctx, action, i), &cycle);
+	}
+	else
+	{
+		pdf_execute_action_imp(ctx, doc, target, path, action);
+		next = pdf_dict_get(ctx, action, PDF_NAME(Next));
+		if (next)
+			pdf_execute_action_chain(ctx, doc, target, path, next, &cycle);
+	}
+}
+
+static void pdf_execute_action(fz_context *ctx, pdf_document *doc, pdf_obj *target, const char *path)
+{
+	pdf_obj *action = pdf_dict_getp_inheritable(ctx, target, path);
+	if (action)
+		pdf_execute_action_chain(ctx, doc, target, path, action, NULL);
+}
+
+void pdf_document_event_will_close(fz_context *ctx, pdf_document *doc)
+{
+	pdf_execute_action(ctx, doc, pdf_trailer(ctx, doc), "Root/AA/WC");
+}
+
+void pdf_document_event_will_save(fz_context *ctx, pdf_document *doc)
+{
+	pdf_execute_action(ctx, doc, pdf_trailer(ctx, doc), "Root/AA/WS");
+}
+
+void pdf_document_event_did_save(fz_context *ctx, pdf_document *doc)
+{
+	pdf_execute_action(ctx, doc, pdf_trailer(ctx, doc), "Root/AA/DS");
+}
+
+void pdf_document_event_will_print(fz_context *ctx, pdf_document *doc)
+{
+	pdf_execute_action(ctx, doc, pdf_trailer(ctx, doc), "Root/AA/WP");
+}
+
+void pdf_document_event_did_print(fz_context *ctx, pdf_document *doc)
+{
+	pdf_execute_action(ctx, doc, pdf_trailer(ctx, doc), "Root/AA/DP");
+}
+
+void pdf_page_event_open(fz_context *ctx, pdf_page *page)
+{
+	pdf_execute_action(ctx, page->doc, page->obj, "AA/O");
+}
+
+void pdf_page_event_close(fz_context *ctx, pdf_page *page)
+{
+	pdf_execute_action(ctx, page->doc, page->obj, "AA/C");
+}
+
+static void
+annot_execute_action(fz_context *ctx, pdf_annot *annot, const char *act)
+{
+	begin_annot_op(ctx, annot, "JavaScript action");
+
+	fz_try(ctx)
+	{
+		pdf_execute_action(ctx, annot->page->doc, annot->obj, act);
+		end_annot_op(ctx, annot);
+	}
+	fz_catch(ctx)
+	{
+		abandon_annot_op(ctx, annot);
+		fz_rethrow(ctx);
+	}
+}
+
+void pdf_annot_event_enter(fz_context *ctx, pdf_annot *annot)
+{
+	annot_execute_action(ctx, annot, "AA/E");
+}
+
+void pdf_annot_event_exit(fz_context *ctx, pdf_annot *annot)
+{
+	annot_execute_action(ctx, annot, "AA/X");
+}
+
+void pdf_annot_event_down(fz_context *ctx, pdf_annot *annot)
+{
+	annot_execute_action(ctx, annot, "AA/D");
+}
+
+void pdf_annot_event_up(fz_context *ctx, pdf_annot *annot)
+{
+	pdf_obj *action;
+
+	begin_annot_op(ctx, annot, "JavaScript action");
+
+	fz_try(ctx)
+	{
+		action = pdf_dict_get(ctx, annot->obj, PDF_NAME(A));
+		if (action)
+			pdf_execute_action_chain(ctx, annot->page->doc, annot->obj, "A", action, NULL);
+		else
+			pdf_execute_action(ctx, annot->page->doc, annot->obj, "AA/U");
+		end_annot_op(ctx, annot);
+	}
+	fz_catch(ctx)
+	{
+		abandon_annot_op(ctx, annot);
+		fz_rethrow(ctx);
+	}
+}
+
+void pdf_annot_event_focus(fz_context *ctx, pdf_annot *annot)
+{
+	annot_execute_action(ctx, annot, "AA/Fo");
+}
+
+void pdf_annot_event_blur(fz_context *ctx, pdf_annot *annot)
+{
+	annot_execute_action(ctx, annot, "AA/Bl");
+}
+
+void pdf_annot_event_page_open(fz_context *ctx, pdf_annot *annot)
+{
+	annot_execute_action(ctx, annot, "AA/PO");
+}
+
+void pdf_annot_event_page_close(fz_context *ctx, pdf_annot *annot)
+{
+	annot_execute_action(ctx, annot, "AA/PC");
+}
+
+void pdf_annot_event_page_visible(fz_context *ctx, pdf_annot *annot)
+{
+	annot_execute_action(ctx, annot, "AA/PV");
+}
+
+void pdf_annot_event_page_invisible(fz_context *ctx, pdf_annot *annot)
+{
+	annot_execute_action(ctx, annot, "AA/PI");
+}
+
+int pdf_field_event_keystroke(fz_context *ctx, pdf_document *doc, pdf_obj *field, pdf_keystroke_event *evt)
+{
+	pdf_js *js = doc->js;
+	if (js)
+	{
+		pdf_obj *action = pdf_dict_getp_inheritable(ctx, field, "AA/K/JS");
+		if (action)
+		{
+			pdf_js_event_init_keystroke(js, field, evt);
+			pdf_execute_js_action(ctx, doc, field, "AA/K/JS", action);
+			return pdf_js_event_result_keystroke(js, evt);
+		}
+	}
+	evt->newChange = fz_strdup(ctx, evt->change);
+	evt->newValue = fz_strdup(ctx, evt->value);
+	return 1;
+}
+
+int pdf_annot_field_event_keystroke(fz_context *ctx, pdf_document *doc, pdf_annot *annot, pdf_keystroke_event *evt)
+{
+	int ret;
+
+	pdf_annot_push_local_xref(ctx, annot);
+
+	fz_try(ctx)
+		ret = pdf_field_event_keystroke(ctx, doc, annot->obj, evt);
+	fz_always(ctx)
+		pdf_annot_pop_local_xref(ctx, annot);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+
+	return ret;
+}
+
+char *pdf_field_event_format(fz_context *ctx, pdf_document *doc, pdf_obj *field)
+{
+	pdf_js *js = doc->js;
+	if (js)
+	{
+		pdf_obj *action = pdf_dict_getp_inheritable(ctx, field, "AA/F/JS");
+		if (action)
+		{
+			const char *value = pdf_field_value(ctx, field);
+			pdf_js_event_init(js, field, value, 1);
+			pdf_execute_js_action(ctx, doc, field, "AA/F/JS", action);
+			return pdf_js_event_value(js);
+		}
+	}
+	return NULL;
+}
+
+int pdf_field_event_validate(fz_context *ctx, pdf_document *doc, pdf_obj *field, const char *value, char **newvalue)
+{
+	pdf_js *js = doc->js;
+
+	*newvalue = NULL;
+	if (js)
+	{
+		pdf_obj *action = pdf_dict_getp_inheritable(ctx, field, "AA/V/JS");
+		if (action)
+		{
+			pdf_js_event_init(js, field, value, 1);
+			pdf_execute_js_action(ctx, doc, field, "AA/V/JS", action);
+			return pdf_js_event_result_validate(js, newvalue);
+		}
+	}
+	return 1;
+}
+
+void pdf_field_event_calculate(fz_context *ctx, pdf_document *doc, pdf_obj *field)
+{
+	pdf_js *js = doc->js;
+	if (js)
+	{
+		pdf_obj *action = pdf_dict_getp_inheritable(ctx, field, "AA/C/JS");
+		if (action)
+		{
+			char *old_value = fz_strdup(ctx, pdf_field_value(ctx, field));
+			char *new_value = NULL;
+			fz_var(new_value);
+			fz_try(ctx)
+			{
+				pdf_js_event_init(js, field, old_value, 1);
+				pdf_execute_js_action(ctx, doc, field, "AA/C/JS", action);
+				if (pdf_js_event_result(js))
+				{
+					new_value = pdf_js_event_value(js);
+					if (strcmp(old_value, new_value))
+						pdf_set_field_value(ctx, doc, field, new_value, 0);
+				}
+			}
+			fz_always(ctx)
+			{
+				fz_free(ctx, old_value);
+				fz_free(ctx, new_value);
+			}
+			fz_catch(ctx)
+				fz_rethrow(ctx);
+		}
+	}
+}
+
+static void
+count_sigs(fz_context *ctx, pdf_obj *field, void *arg, pdf_obj **ft)
+{
+	int *n = (int *)arg;
+
+	if (!pdf_name_eq(ctx, pdf_dict_get(ctx, field, PDF_NAME(Type)), PDF_NAME(Annot)) ||
+		!pdf_name_eq(ctx, pdf_dict_get(ctx, field, PDF_NAME(Subtype)), PDF_NAME(Widget)) ||
+		!pdf_name_eq(ctx, *ft, PDF_NAME(Sig)))
+		return;
+
+	(*n)++;
+}
+
+static pdf_obj *ft_name[2] = { PDF_NAME(FT), NULL };
+
+int pdf_count_signatures(fz_context *ctx, pdf_document *doc)
+{
+	int n = 0;
+	pdf_obj *ft = NULL;
+	pdf_obj *form_fields = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/Fields");
+	pdf_walk_tree(ctx, form_fields, PDF_NAME(Kids), count_sigs, NULL, &n, ft_name, &ft);
+	return n;
+}
+
+/*
+ * Bake interactive form fields into static content.
+ */
+
+static pdf_obj *get_annot_ap(fz_context *ctx, pdf_obj *annot)
+{
+	pdf_obj *ap = pdf_dict_get(ctx, annot, PDF_NAME(AP));
+	pdf_obj *as = pdf_dict_get(ctx, annot, PDF_NAME(AS));
+	if (ap)
+	{
+		as = pdf_resolve_indirect_chain(ctx, as);
+		ap = pdf_dict_get(ctx, ap, PDF_NAME(N));
+		if (pdf_is_stream(ctx, ap))
+			return ap;
+		ap = pdf_dict_get(ctx, ap, as);
+		if (pdf_is_stream(ctx, ap))
+			return ap;
+	}
+	return NULL;
+}
+
+static fz_matrix get_annot_transform(fz_context *ctx, pdf_obj *annot, pdf_obj *ap)
+{
+	float w, h, x, y;
+	fz_matrix transform;
+	fz_rect bbox;
+	fz_rect rect;
+
+	rect = pdf_dict_get_rect(ctx, annot, PDF_NAME(Rect));
+	bbox = pdf_dict_get_rect(ctx, ap, PDF_NAME(BBox));
+	transform = pdf_dict_get_matrix(ctx, ap, PDF_NAME(Matrix));
+
+	bbox = fz_transform_rect(bbox, transform);
+	w = (rect.x1 - rect.x0) / (bbox.x1 - bbox.x0);
+	h = (rect.y1 - rect.y0) / (bbox.y1 - bbox.y0);
+	x = rect.x0 - bbox.x0 * w;
+	y = rect.y0 - bbox.y0 * h;
+
+	return fz_make_matrix(w, 0, 0, h, x, y);
+}
+
+static void pdf_bake_annot(fz_context *ctx, fz_buffer *buf, pdf_document *doc, pdf_obj *page, pdf_obj *res_xobj, pdf_obj *annot)
+{
+	fz_matrix m;
+	pdf_obj *ap;
+	char name[20];
+
+	ap = get_annot_ap(ctx, annot);
+	if (ap)
+	{
+		fz_snprintf(name, sizeof name, "Annot%d", pdf_to_num(ctx, annot));
+		pdf_dict_puts(ctx, res_xobj, name, ap);
+		pdf_dict_put(ctx, ap, PDF_NAME(Type), PDF_NAME(XObject));
+		pdf_dict_put(ctx, ap, PDF_NAME(Subtype), PDF_NAME(Form));
+		m = get_annot_transform(ctx, annot, ap);
+		fz_append_printf(ctx, buf,
+			"q\n%g %g %g %g %g %g cm\n/%s Do\nQ\n",
+			m.a, m.b, m.c, m.d, m.e, m.f,
+			name
+		);
+	}
+}
+
+static void pdf_bake_page(fz_context *ctx, pdf_document *doc, pdf_obj *page, int bake_annots, int bake_widgets)
+{
+	pdf_obj *res;
+	pdf_obj *res_xobj;
+	pdf_obj *contents;
+	pdf_obj *new_contents = NULL;
+	pdf_obj *annots;
+	pdf_obj *annot;
+	pdf_obj *subtype;
+	pdf_obj *prologue = NULL;
+	fz_buffer *buf = NULL;
+	int prepend, append;
+	int i;
+
+	fz_var(buf);
+	fz_var(prologue);
+	fz_var(new_contents);
+
+	annots = pdf_dict_get(ctx, page, PDF_NAME(Annots));
+	if (pdf_array_len(ctx, annots) == 0)
+		return;
+
+	res = pdf_dict_get(ctx, page, PDF_NAME(Resources));
+	if (!res)
+		res = pdf_dict_put_dict(ctx, page, PDF_NAME(Resources), 4);
+
+	res_xobj = pdf_dict_get(ctx, res, PDF_NAME(XObject));
+	if (!res_xobj)
+		res_xobj = pdf_dict_put_dict(ctx, res, PDF_NAME(XObject), 8);
+
+	fz_try(ctx)
+	{
+		// Ensure that the graphics state is balanced.
+		contents = pdf_dict_get(ctx, page, PDF_NAME(Contents));
+		pdf_count_q_balance(ctx, doc, res, contents, &prepend, &append);
+
+		if (prepend)
+		{
+			// Prepend enough 'q' to ensure we can get back to initial state.
+			buf = fz_new_buffer(ctx, 1024);
+			while (prepend-- > 0)
+				fz_append_string(ctx, buf, "q\n");
+
+			prologue = pdf_add_stream(ctx, doc, buf, NULL, 0);
+			fz_drop_buffer(ctx, buf);
+			buf = NULL;
+		}
+
+		// Append enough 'Q' to get back to initial state.
+		buf = fz_new_buffer(ctx, 1024);
+		while (append-- > 0)
+			fz_append_string(ctx, buf, "Q\n");
+
+		for (i = 0; i < pdf_array_len(ctx, annots); )
+		{
+			annot = pdf_array_get(ctx, annots, i);
+			subtype = pdf_dict_get(ctx, annot, PDF_NAME(Subtype));
+			if (subtype == PDF_NAME(Link))
+			{
+				++i;
+			}
+			else if (subtype == PDF_NAME(Widget))
+			{
+				if (bake_widgets)
+				{
+					pdf_bake_annot(ctx, buf, doc, page, res_xobj, annot);
+					pdf_array_delete(ctx, annots, i);
+				}
+				else
+				{
+					++i;
+				}
+			}
+			else
+			{
+				if (bake_annots)
+				{
+					pdf_bake_annot(ctx, buf, doc, page, res_xobj, annot);
+					pdf_array_delete(ctx, annots, i);
+				}
+				else
+				{
+					++i;
+				}
+			}
+		}
+
+		if (!pdf_is_array(ctx, contents))
+		{
+			new_contents = pdf_new_array(ctx, doc, 10);
+			if (prologue)
+				pdf_array_push(ctx, new_contents, prologue);
+			if (contents)
+				pdf_array_push(ctx, new_contents, contents);
+			pdf_dict_put(ctx, page, PDF_NAME(Contents), new_contents);
+			pdf_drop_obj(ctx, new_contents);
+			contents = new_contents;
+			new_contents = NULL;
+		}
+		else if (prologue)
+		{
+			pdf_array_insert(ctx, contents, prologue, 0);
+		}
+
+		pdf_array_push_drop(ctx, contents, pdf_add_stream(ctx, doc, buf, NULL, 0));
+	}
+	fz_always(ctx)
+	{
+		fz_drop_buffer(ctx, buf);
+		pdf_drop_obj(ctx, prologue);
+		pdf_drop_obj(ctx, new_contents);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow(ctx);
+	}
+}
+
+void pdf_bake_document(fz_context *ctx, pdf_document *doc, int bake_annots, int bake_widgets)
+{
+	pdf_page *page = NULL;
+	pdf_annot *annot;
+	int i, n;
+
+	fz_var(page);
+
+	pdf_begin_operation(ctx, doc, "Bake interactive content");
+	fz_try(ctx)
+	{
+		n = pdf_count_pages(ctx, doc);
+		for (i = 0; i < n; ++i)
+		{
+			page = pdf_load_page(ctx, doc, i);
+
+			if (bake_annots)
+				for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot))
+					pdf_annot_request_synthesis(ctx, annot);
+			if (bake_widgets)
+				for (annot = pdf_first_widget(ctx, page); annot; annot = pdf_next_widget(ctx, annot))
+					pdf_annot_request_synthesis(ctx, annot);
+			pdf_update_page(ctx, page);
+
+			pdf_bake_page(ctx, doc, page->obj, bake_annots, bake_widgets);
+
+			fz_drop_page(ctx, (fz_page*)page);
+			page = NULL;
+		}
+
+		if (bake_widgets)
+		{
+			pdf_obj *trailer = pdf_trailer(ctx, doc);
+			pdf_obj *root = pdf_dict_get(ctx, trailer, PDF_NAME(Root));
+			pdf_dict_del(ctx, root, PDF_NAME(AcroForm));
+		}
+		pdf_end_operation(ctx, doc);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_page(ctx, (fz_page*)page);
+	}
+	fz_catch(ctx)
+	{
+		pdf_abandon_operation(ctx, doc);
+	}
+}