view src_classic/helper-fields.i @ 1:1d09e1dec1d9 upstream

ADD: PyMuPDF v1.26.4: the original sdist. It does not yet contain MuPDF. This normally will be downloaded when building PyMuPDF.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:37:51 +0200
parents
children
line wrap: on
line source

%{
/*
# ------------------------------------------------------------------------
# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com
# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html
#
# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a
# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is
# maintained and developed by Artifex Software, Inc. https://artifex.com.
# ------------------------------------------------------------------------
*/
#define SETATTR(a, v) PyObject_SetAttrString(Widget, a, v)
#define GETATTR(a) PyObject_GetAttrString(Widget, a)
#define CALLATTR(m, p) PyObject_CallMethod(Widget, m, p)

static void
SETATTR_DROP(PyObject *mod, const char *attr, PyObject *value)
{
    if (!value)
        PyObject_DelAttrString(mod, attr);
    else
    {
        PyObject_SetAttrString(mod, attr, value);
        Py_DECREF(value);
    }
}

//-----------------------------------------------------------------------------
// Functions dealing with PDF form fields (widgets)
//-----------------------------------------------------------------------------
enum
{
	SigFlag_SignaturesExist = 1,
	SigFlag_AppendOnly = 2
};


// make new PDF action object from JavaScript source
// Parameters are a PDF document and a Python string.
// Returns a PDF action object.
//-----------------------------------------------------------------------------
pdf_obj *
JM_new_javascript(fz_context *ctx, pdf_document *pdf, PyObject *value)
{
    fz_buffer *res = NULL;
    if (!PyObject_IsTrue(value))  // no argument given
        return NULL;

    char *data = JM_StrAsChar(value);
    if (!data)  // not convertible to char*
        return NULL;

    res = fz_new_buffer_from_copied_data(ctx, data, strlen(data));
    pdf_obj *source = pdf_add_stream(ctx, pdf, res, NULL, 0);
    pdf_obj *newaction = pdf_add_new_dict(ctx, pdf, 4);
    pdf_dict_put(ctx, newaction, PDF_NAME(S), pdf_new_name(ctx, "JavaScript"));
    pdf_dict_put(ctx, newaction, PDF_NAME(JS), source);
    fz_drop_buffer(ctx, res);
    return pdf_keep_obj(ctx, newaction);
}


// JavaScript extractor
// Returns either the script source or None. Parameter is a PDF action
// dictionary, which must have keys /S and /JS. The value of /S must be
// '/JavaScript'. The value of /JS is returned.
//-----------------------------------------------------------------------------
PyObject *
JM_get_script(fz_context *ctx, pdf_obj *key)
{
    pdf_obj *js = NULL;
    fz_buffer *res = NULL;
    PyObject *script = NULL;
    if (!key) Py_RETURN_NONE;

    if (!strcmp(pdf_to_name(ctx,
                pdf_dict_get(ctx, key, PDF_NAME(S))), "JavaScript")) {
        js = pdf_dict_get(ctx, key, PDF_NAME(JS));
    }
    if (!js) Py_RETURN_NONE;

    if (pdf_is_string(ctx, js)) {
        script = JM_UnicodeFromStr(pdf_to_text_string(ctx, js));
    } else if (pdf_is_stream(ctx, js)) {
        res = pdf_load_stream(ctx, js);
        script = JM_EscapeStrFromBuffer(ctx, res);
        fz_drop_buffer(ctx, res);
    } else {
        Py_RETURN_NONE;
    }
    if (PyObject_IsTrue(script)) { // do not return an empty script
        return script;
    }
    Py_CLEAR(script);
    Py_RETURN_NONE;
}


// Create a JavaScript PDF action.
// Usable for all object types which support PDF actions, even if the
// argument name suggests annotations. Up to 2 key values can be specified, so
// JavaScript actions can be stored for '/A' and '/AA/?' keys.
//-----------------------------------------------------------------------------
void JM_put_script(fz_context *ctx, pdf_obj *annot_obj, pdf_obj *key1, pdf_obj *key2, PyObject *value)
{
    PyObject *script = NULL;
    pdf_obj *key1_obj = pdf_dict_get(ctx, annot_obj, key1);
    pdf_document *pdf = pdf_get_bound_document(ctx, annot_obj);  // owning PDF

    // if no new script given, just delete corresponding key
    if (!value || !PyObject_IsTrue(value)) {
        if (!key2) {
            pdf_dict_del(ctx, annot_obj, key1);
        } else if (key1_obj) {
            pdf_dict_del(ctx, key1_obj, key2);
        }
        return;
    }

    // read any existing script as a PyUnicode string
    if (!key2 || !key1_obj) {
        script = JM_get_script(ctx, key1_obj);
    } else {
        script = JM_get_script(ctx, pdf_dict_get(ctx, key1_obj, key2));
    }

    // replace old script, if different from new one
    if (!PyObject_RichCompareBool(value, script, Py_EQ)) {
        pdf_obj *newaction = JM_new_javascript(ctx, pdf, value);
        if (!key2) {
            pdf_dict_put_drop(ctx, annot_obj, key1, newaction);
        } else {
            pdf_dict_putl_drop(ctx, annot_obj, newaction, key1, key2, NULL);
        }
    }
    Py_XDECREF(script);
    return;
}

/*
// Execute a JavaScript action for annot or field.
//-----------------------------------------------------------------------------
PyObject *
JM_exec_script(fz_context *ctx, pdf_obj *annot_obj, pdf_obj *key1, pdf_obj *key2)
{
    PyObject *script = NULL;
    char *code = NULL;
    fz_try(ctx) {
        pdf_document *pdf = pdf_get_bound_document(ctx, annot_obj);
        char buf[100];
        if (!key2) {
            script = JM_get_script(ctx, key1_obj);
        } else {
            script = JM_get_script(ctx, pdf_dict_get(ctx, key1_obj, key2));
        }
        code = JM_StrAsChar(script);
        fz_snprintf(buf, sizeof buf, "%d/A", pdf_to_num(ctx, annot_obj));
        pdf_js_execute(pdf->js, buf, code);
    }
    fz_always(ctx) {
        Py_XDECREF(string);
    }
    fz_catch(ctx) {
        Py_RETURN_FALSE;
    }
    Py_RETURN_TRUE;
}
*/

// String from widget type
//-----------------------------------------------------------------------------
char *JM_field_type_text(int wtype)
{
    switch(wtype) {
        case(PDF_WIDGET_TYPE_BUTTON):
            return "Button";
        case(PDF_WIDGET_TYPE_CHECKBOX):
            return "CheckBox";
        case(PDF_WIDGET_TYPE_RADIOBUTTON):
            return "RadioButton";
        case(PDF_WIDGET_TYPE_TEXT):
            return "Text";
        case(PDF_WIDGET_TYPE_LISTBOX):
            return "ListBox";
        case(PDF_WIDGET_TYPE_COMBOBOX):
            return "ComboBox";
        case(PDF_WIDGET_TYPE_SIGNATURE):
            return "Signature";
        default:
            return "unknown";
    }
}

// Set the field type
//-----------------------------------------------------------------------------
void JM_set_field_type(fz_context *ctx, pdf_document *doc, pdf_obj *obj, int type)
{
	int setbits = 0;
	int clearbits = 0;
	pdf_obj *typename = NULL;

	switch(type) {
	case PDF_WIDGET_TYPE_BUTTON:
		typename = PDF_NAME(Btn);
		setbits = PDF_BTN_FIELD_IS_PUSHBUTTON;
		break;
	case PDF_WIDGET_TYPE_RADIOBUTTON:
		typename = PDF_NAME(Btn);
		clearbits = PDF_BTN_FIELD_IS_PUSHBUTTON;
		setbits = PDF_BTN_FIELD_IS_RADIO;
		break;
	case PDF_WIDGET_TYPE_CHECKBOX:
		typename = PDF_NAME(Btn);
		clearbits = (PDF_BTN_FIELD_IS_PUSHBUTTON|PDF_BTN_FIELD_IS_RADIO);
		break;
	case PDF_WIDGET_TYPE_TEXT:
		typename = PDF_NAME(Tx);
		break;
	case PDF_WIDGET_TYPE_LISTBOX:
		typename = PDF_NAME(Ch);
		clearbits = PDF_CH_FIELD_IS_COMBO;
		break;
	case PDF_WIDGET_TYPE_COMBOBOX:
		typename = PDF_NAME(Ch);
		setbits = PDF_CH_FIELD_IS_COMBO;
		break;
	case PDF_WIDGET_TYPE_SIGNATURE:
		typename = PDF_NAME(Sig);
		break;
	}

	if (typename)
		pdf_dict_put_drop(ctx, obj, PDF_NAME(FT), typename);

	if (setbits != 0 || clearbits != 0) {
		int bits = pdf_dict_get_int(ctx, obj, PDF_NAME(Ff));
		bits &= ~clearbits;
		bits |= setbits;
		pdf_dict_put_int(ctx, obj, PDF_NAME(Ff), bits);
	}
}

// Copied from MuPDF v1.14
// Create widget.
// Returns a kept reference to a pdf_annot - caller must drop it.
//-----------------------------------------------------------------------------
pdf_annot *JM_create_widget(fz_context *ctx, pdf_document *doc, pdf_page *page, int type, char *fieldname)
{
	pdf_obj *form = NULL;
	int old_sigflags = pdf_to_int(ctx, pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/SigFlags"));
	pdf_annot *annot = pdf_create_annot_raw(ctx, page, PDF_ANNOT_WIDGET);   // returns a kept reference.
    pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
	fz_try(ctx) {
		JM_set_field_type(ctx, doc, annot_obj, type);
		pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(T), fieldname);

		if (type == PDF_WIDGET_TYPE_SIGNATURE) {
			int sigflags = (old_sigflags | (SigFlag_SignaturesExist|SigFlag_AppendOnly));
			pdf_dict_putl_drop(ctx, pdf_trailer(ctx, doc), pdf_new_int(ctx, sigflags), PDF_NAME(Root), PDF_NAME(AcroForm), PDF_NAME(SigFlags), NULL);
		}

		/*
		pdf_create_annot will have linked the new widget into the page's
		annot array. We also need it linked into the document's form
		*/
		form = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/Fields");
		if (!form) {
			form = pdf_new_array(ctx, doc, 1);
			pdf_dict_putl_drop(ctx, pdf_trailer(ctx, doc),
                               form,
                               PDF_NAME(Root),
                               PDF_NAME(AcroForm),
                               PDF_NAME(Fields),
                               NULL);
		}

		pdf_array_push(ctx, form, annot_obj); // Cleanup relies on this statement being last
	}
	fz_catch(ctx) {
		pdf_delete_annot(ctx, page, annot);

		if (type == PDF_WIDGET_TYPE_SIGNATURE) {
			pdf_dict_putl_drop(ctx, pdf_trailer(ctx, doc), pdf_new_int(ctx, old_sigflags), PDF_NAME(Root), PDF_NAME(AcroForm), PDF_NAME(SigFlags), NULL);
        }

		fz_rethrow(ctx);
	}

	return annot;
}



// PushButton get state
//-----------------------------------------------------------------------------
PyObject *JM_pushbtn_state(fz_context *ctx, pdf_annot *annot)
{   // pushed buttons do not reflect status changes in the PDF
    // always reflect them as untouched
    Py_RETURN_FALSE;
}


// Text field retrieve value
//-----------------------------------------------------------------------------
PyObject *JM_text_value(fz_context *ctx, pdf_annot *annot)
{
    const char *text = NULL;
    fz_var(text);
    fz_try(ctx) {
        pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
        text = pdf_field_value(ctx, annot_obj);
    }
    fz_catch(ctx) Py_RETURN_NONE;
    return JM_UnicodeFromStr(text);
}

// ListBox retrieve value
//-----------------------------------------------------------------------------
PyObject *JM_listbox_value(fz_context *ctx, pdf_annot *annot)
{
    int i = 0, n = 0;
    // may be single value or array
    pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
    pdf_obj *optarr = pdf_dict_get(ctx, annot_obj, PDF_NAME(V));
    if (pdf_is_string(ctx, optarr))         // a single string
        return PyString_FromString(pdf_to_text_string(ctx, optarr));

    // value is an array (may have len 0)
    n = pdf_array_len(ctx, optarr);
    PyObject *liste = PyList_New(0);

    // extract a list of strings
    // each entry may again be an array: take second entry then
    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);
        LIST_APPEND_DROP(liste, JM_UnicodeFromStr(pdf_to_text_string(ctx, elem)));
    }
    return liste;
}

// ComboBox retrieve value
//-----------------------------------------------------------------------------
PyObject *JM_combobox_value(fz_context *ctx, pdf_annot *annot)
{   // combobox treated like listbox
    return JM_listbox_value(ctx, annot);
}

// Signature field retrieve value
PyObject *JM_signature_value(fz_context *ctx, pdf_annot *annot)
{   // signatures are currently not supported
    Py_RETURN_NONE;
}

// retrieve ListBox / ComboBox choice values
//-----------------------------------------------------------------------------
PyObject *JM_choice_options(fz_context *ctx, pdf_annot *annot)
{   // return list of choices for list or combo boxes
    pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
    PyObject *val;
    int n = pdf_choice_widget_options(ctx, annot, 0, NULL);
    if (n == 0) Py_RETURN_NONE;                     // wrong widget type

    pdf_obj *optarr = pdf_dict_get(ctx, annot_obj, PDF_NAME(Opt));
    int i, m;
    PyObject *liste = PyList_New(0);

    for (i = 0; i < n; i++) {
        m = pdf_array_len(ctx, pdf_array_get(ctx, optarr, i));
        if (m == 2) {
            val = Py_BuildValue("ss",
            pdf_to_text_string(ctx, pdf_array_get(ctx, pdf_array_get(ctx, optarr, i), 0)),
            pdf_to_text_string(ctx, pdf_array_get(ctx, pdf_array_get(ctx, optarr, i), 1)));
            LIST_APPEND_DROP(liste, val);
        } else {
            val = JM_UnicodeFromStr(pdf_to_text_string(ctx, pdf_array_get(ctx, optarr, i)));
            LIST_APPEND_DROP(liste, val);
        }
    }
    return liste;
}


// set ListBox / ComboBox values
//-----------------------------------------------------------------------------
void JM_set_choice_options(fz_context *ctx, pdf_annot *annot, PyObject *liste)
{
    if (!liste) return;
    if (!PySequence_Check(liste)) return;
    Py_ssize_t i, n = PySequence_Size(liste);
    if (n < 1) return;
    PyObject *tuple = PySequence_Tuple(liste);
    PyObject *val = NULL, *val1 = NULL, *val2 = NULL;
    pdf_obj *optarrsub = NULL, *optarr = NULL, *annot_obj = NULL;
    pdf_document *pdf = NULL;
    const char *opt = NULL, *opt1 = NULL, *opt2 = NULL;
    fz_try(ctx) {
        annot_obj = pdf_annot_obj(ctx, annot);
        pdf = pdf_get_bound_document(ctx, annot_obj);
        optarr = pdf_new_array(ctx, pdf, (int) n);
    for (i = 0; i < n; i++) {
        val = PyTuple_GET_ITEM(tuple, i);
        opt = PyUnicode_AsUTF8(val);
        if (opt) {
            pdf_array_push_text_string(ctx, optarr, opt);
        } else {
                if (!PySequence_Check(val) || PySequence_Size(val) != 2) {
                    RAISEPY(ctx, "bad choice field list", PyExc_ValueError);
                }
                val1 = PySequence_GetItem(val, 0);
                opt1 = PyUnicode_AsUTF8(val1);
                if (!opt1) {
                    RAISEPY(ctx, "bad choice field list", PyExc_ValueError);
                }
                val2 = PySequence_GetItem(val, 1);
                opt2 = PyUnicode_AsUTF8(val2);
                if (!opt2) {
                    RAISEPY(ctx, "bad choice field list", PyExc_ValueError);
                };
                Py_CLEAR(val1);
                Py_CLEAR(val2);
            optarrsub = pdf_array_push_array(ctx, optarr, 2);
            pdf_array_push_text_string(ctx, optarrsub, opt1);
            pdf_array_push_text_string(ctx, optarrsub, opt2);
        }
    }
    pdf_dict_put_drop(ctx, annot_obj, PDF_NAME(Opt), optarr);
    }
    fz_always(ctx) {
        Py_CLEAR(tuple);
        Py_CLEAR(val1);
        Py_CLEAR(val2);
        PyErr_Clear();
    }
    fz_catch(ctx) {
        fz_rethrow(ctx);
    }
    return;
}


//-----------------------------------------------------------------------------
// Populate a Python Widget object with the values from a PDF form field.
// Called by "Page.firstWidget" and "Widget.next".
//-----------------------------------------------------------------------------
void JM_get_widget_properties(fz_context *ctx, pdf_annot *annot, PyObject *Widget)
{
    pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
    pdf_page *page = pdf_annot_page(ctx, annot);
    pdf_document *pdf = page->doc;
    pdf_annot *tw = annot;
    pdf_obj *obj = NULL;
    Py_ssize_t i = 0, n = 0;
    fz_try(ctx) {
        int field_type = pdf_widget_type(ctx, tw);
        SETATTR_DROP(Widget, "field_type", Py_BuildValue("i", field_type));
        if (field_type == PDF_WIDGET_TYPE_SIGNATURE) {
            if (pdf_signature_is_signed(ctx, pdf, annot_obj)) {
                SETATTR("is_signed", Py_True);
            } else {
                SETATTR("is_signed", Py_False);
            }
        } else {
            SETATTR("is_signed", Py_None);
        }
        SETATTR_DROP(Widget, "border_style",
                JM_UnicodeFromStr(pdf_field_border_style(ctx, annot_obj)));
        SETATTR_DROP(Widget, "field_type_string",
                JM_UnicodeFromStr(JM_field_type_text(field_type)));

        #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR <= 22
            char *field_name = pdf_field_name(ctx, annot_obj);
        #else
            char *field_name = pdf_load_field_name(ctx, annot_obj);
        #endif
        SETATTR_DROP(Widget, "field_name", JM_UnicodeFromStr(field_name));
        JM_Free(field_name);

        const char *label = NULL;
        obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(TU));
        if (obj) label = pdf_to_text_string(ctx, obj);
        SETATTR_DROP(Widget, "field_label", JM_UnicodeFromStr(label));

        const char *fvalue = NULL;
        if (field_type == PDF_WIDGET_TYPE_RADIOBUTTON) {
            obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(Parent));  // owning RB group
            if (obj) {
                SETATTR_DROP(Widget, "rb_parent", Py_BuildValue("i", pdf_to_num(ctx, obj)));
            }
            obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(AS));
            if (obj) {
                fvalue = pdf_to_name(ctx, obj);
            }
        }
        if (!fvalue) {
            fvalue = pdf_field_value(ctx, annot_obj);
        }
        SETATTR_DROP(Widget, "field_value", JM_UnicodeFromStr(fvalue));

        SETATTR_DROP(Widget, "field_display",
                Py_BuildValue("i", pdf_field_display(ctx, annot_obj)));

        float border_width = pdf_to_real(ctx, pdf_dict_getl(ctx, annot_obj,
                                PDF_NAME(BS), PDF_NAME(W), NULL));
        if (border_width == 0) border_width = 1;
        SETATTR_DROP(Widget, "border_width",
                Py_BuildValue("f", border_width));

        obj = pdf_dict_getl(ctx, annot_obj,
                                PDF_NAME(BS), PDF_NAME(D), NULL);
        if (pdf_is_array(ctx, obj)) {
            n = (Py_ssize_t) pdf_array_len(ctx, obj);
            PyObject *d = PyList_New(n);
            for (i = 0; i < n; i++) {
                PyList_SET_ITEM(d, i, Py_BuildValue("i", pdf_to_int(ctx,
                                pdf_array_get(ctx, obj, (int) i))));
            }
            SETATTR_DROP(Widget, "border_dashes", d);
        }

        SETATTR_DROP(Widget, "text_maxlen",
                Py_BuildValue("i", pdf_text_widget_max_len(ctx, tw)));

        SETATTR_DROP(Widget, "text_format",
                Py_BuildValue("i", pdf_text_widget_format(ctx, tw)));

        obj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(MK), PDF_NAME(BG), NULL);
        if (pdf_is_array(ctx, obj)) {
            n = (Py_ssize_t) pdf_array_len(ctx, obj);
            PyObject *col = PyList_New(n);
            for (i = 0; i < n; i++) {
                PyList_SET_ITEM(col, i, Py_BuildValue("f",
                pdf_to_real(ctx, pdf_array_get(ctx, obj, (int) i))));
            }
            SETATTR_DROP(Widget, "fill_color", col);
        }

        obj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(MK), PDF_NAME(BC), NULL);
        if (pdf_is_array(ctx, obj)) {
            n = (Py_ssize_t) pdf_array_len(ctx, obj);
            PyObject *col = PyList_New(n);
            for (i = 0; i < n; i++) {
                PyList_SET_ITEM(col, i, Py_BuildValue("f",
                pdf_to_real(ctx, pdf_array_get(ctx, obj, (int) i))));
            }
            SETATTR_DROP(Widget, "border_color", col);
        }

        SETATTR_DROP(Widget, "choice_values", JM_choice_options(ctx, annot));

        const char *da = pdf_to_text_string(ctx, pdf_dict_get_inheritable(ctx,
                                        annot_obj, PDF_NAME(DA)));
        SETATTR_DROP(Widget, "_text_da", JM_UnicodeFromStr(da));

        obj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(MK), PDF_NAME(CA), NULL);
        if (obj) {
            SETATTR_DROP(Widget, "button_caption",
                    JM_UnicodeFromStr((char *)pdf_to_text_string(ctx, obj)));
        }

        SETATTR_DROP(Widget, "field_flags",
                Py_BuildValue("i", pdf_field_flags(ctx, annot_obj)));

        // call Py method to reconstruct text color, font name, size
        PyObject *call = CALLATTR("_parse_da", NULL);
        Py_XDECREF(call);

        // extract JavaScript action texts
        SETATTR_DROP(Widget, "script",
            JM_get_script(ctx, pdf_dict_get(ctx, annot_obj, PDF_NAME(A))));

        SETATTR_DROP(Widget, "script_stroke",
            JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(K), NULL)));

        SETATTR_DROP(Widget, "script_format",
            JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(F), NULL)));

        SETATTR_DROP(Widget, "script_change",
            JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(V), NULL)));

        SETATTR_DROP(Widget, "script_calc",
            JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(C), NULL)));

        SETATTR_DROP(Widget, "script_blur",
            JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), pdf_new_name(ctx, "Bl"), NULL)));

        SETATTR_DROP(Widget, "script_focus",
            JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), pdf_new_name(ctx, "Fo"), NULL)));
    }
    fz_always(ctx) PyErr_Clear();
    fz_catch(ctx) fz_rethrow(ctx);
    return;
}


//-----------------------------------------------------------------------------
// Update the PDF form field with the properties from a Python Widget object.
// Called by "Page.addWidget" and "Annot.updateWidget".
//-----------------------------------------------------------------------------
void JM_set_widget_properties(fz_context *ctx, pdf_annot *annot, PyObject *Widget)
{
    pdf_page *page = pdf_annot_page(ctx, annot);
    pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
    pdf_document *pdf = page->doc;
    fz_rect rect;
    pdf_obj *fill_col = NULL, *border_col = NULL;
    pdf_obj *dashes = NULL;
    Py_ssize_t i, n = 0;
    int d;
    PyObject *value = GETATTR("field_type");
    int field_type = (int) PyInt_AsLong(value);
    Py_DECREF(value);

    // rectangle --------------------------------------------------------------
    value = GETATTR("rect");
    rect = JM_rect_from_py(value);
    Py_XDECREF(value);
    fz_matrix rot_mat = JM_rotate_page_matrix(ctx, page);
    rect = fz_transform_rect(rect, rot_mat);
    pdf_set_annot_rect(ctx, annot, rect);

    // fill color -------------------------------------------------------------
    value = GETATTR("fill_color");
    if (value && PySequence_Check(value)) {
        n = PySequence_Size(value);
        fill_col = pdf_new_array(ctx, pdf, n);
        double col = 0;
        for (i = 0; i < n; i++) {
            JM_FLOAT_ITEM(value, i, &col);
            pdf_array_push_real(ctx, fill_col, col);
        }
        pdf_field_set_fill_color(ctx, annot_obj, fill_col);
        pdf_drop_obj(ctx, fill_col);
    }
    Py_XDECREF(value);

    // dashes -----------------------------------------------------------------
    value = GETATTR("border_dashes");
    if (value && PySequence_Check(value)) {
        n = PySequence_Size(value);
        dashes = pdf_new_array(ctx, pdf, n);
        for (i = 0; i < n; i++) {
            pdf_array_push_int(ctx, dashes,
                               (int64_t) PyInt_AsLong(PySequence_ITEM(value, i)));
        }
        pdf_dict_putl_drop(ctx, annot_obj, dashes,
                                PDF_NAME(BS),
                                PDF_NAME(D),
                                NULL);
    }
    Py_XDECREF(value);

    // border color -----------------------------------------------------------
    value = GETATTR("border_color");
    if (value && PySequence_Check(value)) {
        n = PySequence_Size(value);
        border_col = pdf_new_array(ctx, pdf, n);
        double col = 0;
        for (i = 0; i < n; i++) {
            JM_FLOAT_ITEM(value, i, &col);
            pdf_array_push_real(ctx, border_col, col);
        }
        pdf_dict_putl_drop(ctx, annot_obj, border_col,
                                PDF_NAME(MK),
                                PDF_NAME(BC),
                                NULL);
    }
    Py_XDECREF(value);

    // entry ignored - may be used later
    /*
    int text_format = (int) PyInt_AsLong(GETATTR("text_format"));
    */

    // field label -----------------------------------------------------------
    value = GETATTR("field_label");
    if (value != Py_None) {
        char *label = JM_StrAsChar(value);
        pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(TU), label);
    }
    Py_XDECREF(value);

    // field name -------------------------------------------------------------
    value = GETATTR("field_name");
    if (value != Py_None) {
        char *name = JM_StrAsChar(value);
        #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR <= 22
            char *old_name = pdf_field_name(ctx, annot_obj);
        #else
            char *old_name = pdf_load_field_name(ctx, annot_obj);
        #endif
        if (strcmp(name, old_name) != 0) {
            pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(T), name);
        }
        JM_Free(old_name);
    }
    Py_XDECREF(value);

    // max text len -----------------------------------------------------------
    if (field_type == PDF_WIDGET_TYPE_TEXT)
    {
        value = GETATTR("text_maxlen");
        int text_maxlen = (int) PyInt_AsLong(value);
        if (text_maxlen) {
            pdf_dict_put_int(ctx, annot_obj, PDF_NAME(MaxLen), text_maxlen);
        }
        Py_XDECREF(value);
    }
    value = GETATTR("field_display");
    d = (int) PyInt_AsLong(value);
    Py_XDECREF(value);
    pdf_field_set_display(ctx, annot_obj, d);

    // choice values ----------------------------------------------------------
    if (field_type == PDF_WIDGET_TYPE_LISTBOX ||
        field_type == PDF_WIDGET_TYPE_COMBOBOX) {
        value = GETATTR("choice_values");
        JM_set_choice_options(ctx, annot, value);
        Py_XDECREF(value);
    }

    // border style -----------------------------------------------------------
    value = GETATTR("border_style");
    pdf_obj *val = JM_get_border_style(ctx, value);
    Py_XDECREF(value);
    pdf_dict_putl_drop(ctx, annot_obj, val,
                            PDF_NAME(BS),
                            PDF_NAME(S),
                            NULL);

    // border width -----------------------------------------------------------
    value = GETATTR("border_width");
    float border_width = (float) PyFloat_AsDouble(value);
    Py_XDECREF(value);
    pdf_dict_putl_drop(ctx, annot_obj, pdf_new_real(ctx, border_width),
                            PDF_NAME(BS),
                            PDF_NAME(W),
                            NULL);

    // /DA string -------------------------------------------------------------
    value = GETATTR("_text_da");
    char *da = JM_StrAsChar(value);
    Py_XDECREF(value);
    pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(DA), da);
    pdf_dict_del(ctx, annot_obj, PDF_NAME(DS)); /* not supported by MuPDF */
    pdf_dict_del(ctx, annot_obj, PDF_NAME(RC)); /* not supported by MuPDF */

    // field flags ------------------------------------------------------------
    value = GETATTR("field_flags");
    int field_flags = (int) PyInt_AsLong(value);
    Py_XDECREF(value);
    if (!PyErr_Occurred()) {
        if (field_type == PDF_WIDGET_TYPE_COMBOBOX) {
            field_flags |= PDF_CH_FIELD_IS_COMBO;
        } else if (field_type == PDF_WIDGET_TYPE_RADIOBUTTON) {
            field_flags |= PDF_BTN_FIELD_IS_RADIO;
        } else if (field_type == PDF_WIDGET_TYPE_BUTTON) {
            field_flags |= PDF_BTN_FIELD_IS_PUSHBUTTON;
        }
        pdf_dict_put_int(ctx, annot_obj, PDF_NAME(Ff), field_flags);
    }

    // button caption ---------------------------------------------------------
    value = GETATTR("button_caption");
    char *ca = JM_StrAsChar(value);
    if (ca) {
        pdf_field_set_button_caption(ctx, annot_obj, ca);
    }
    Py_XDECREF(value);

    // script (/A) -------------------------------------------------------
    value = GETATTR("script");
    JM_put_script(ctx, annot_obj, PDF_NAME(A), NULL, value);
    Py_CLEAR(value);

    // script (/AA/K) -------------------------------------------------------
    value = GETATTR("script_stroke");
    JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(K), value);
    Py_CLEAR(value);

    // script (/AA/F) -------------------------------------------------------
    value = GETATTR("script_format");
    JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(F), value);
    Py_CLEAR(value);

    // script (/AA/V) -------------------------------------------------------
    value = GETATTR("script_change");
    JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(V), value);
    Py_CLEAR(value);

    // script (/AA/C) -------------------------------------------------------
    value = GETATTR("script_calc");
    JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(C), value);
    Py_CLEAR(value);

    // script (/AA/Bl) ------------------------------------------------------
    value = GETATTR("script_blur");
    JM_put_script(ctx, annot_obj, PDF_NAME(AA), pdf_new_name(ctx, "Bl"), value);
    Py_CLEAR(value);

    // script (/AA/Fo) ------------------------------------------------------
    value = GETATTR("script_focus");
    JM_put_script(ctx, annot_obj, PDF_NAME(AA), pdf_new_name(ctx, "Fo"), value);
    Py_CLEAR(value);

    // field value ------------------------------------------------------------
    value = GETATTR("field_value");  // field value
    char *text = JM_StrAsChar(value);  // convert to text (may fail!)

    switch(field_type)
    {
    case PDF_WIDGET_TYPE_RADIOBUTTON:
        if (PyObject_RichCompareBool(value, Py_False, Py_EQ)) {
            pdf_set_field_value(ctx, pdf, annot_obj, "Off", 1);
            pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), "Off");
        } else {
            pdf_obj *onstate = pdf_button_field_on_state(ctx, annot_obj);
            if (onstate) {
                const char *on = pdf_to_name(ctx, onstate);
                pdf_set_field_value(ctx, pdf, annot_obj, on, 1);
                pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), on);
            } else  if (text) {
                pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), text);
            }
        }
        break;

    case PDF_WIDGET_TYPE_CHECKBOX:  // will always be "Yes" or "Off"
        if (PyObject_RichCompareBool(value, Py_True, Py_EQ) || text && strcmp(text, "Yes")==0) {
            pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), "Yes");
            pdf_dict_put_name(gctx, annot_obj, PDF_NAME(V), "Yes");
        } else {
            pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), "Off");
            pdf_dict_put_name(gctx, annot_obj, PDF_NAME(V), "Off");
        }
        break;

    default:
        if (text) {
            pdf_set_field_value(ctx, pdf, annot_obj, (const char *)text, 1);
            if (field_type == PDF_WIDGET_TYPE_COMBOBOX || field_type == PDF_WIDGET_TYPE_LISTBOX) {
                pdf_dict_del(ctx, annot_obj, PDF_NAME(I));
            }
        }
    }
    Py_CLEAR(value);
    PyErr_Clear();
    pdf_dirty_annot(ctx, annot);
    pdf_set_annot_hot(ctx, annot, 1);
    pdf_set_annot_active(ctx, annot, 1);
    pdf_update_annot(ctx, annot);
}
#undef SETATTR
#undef GETATTR
#undef CALLATTR
%}

%pythoncode %{
#------------------------------------------------------------------------------
# Class describing a PDF form field ("widget")
#------------------------------------------------------------------------------
class Widget(object):
    def __init__(self):
        self.thisown = True
        self.border_color = None
        self.border_style = "S"
        self.border_width = 0
        self.border_dashes = None
        self.choice_values = None  # choice fields only
        self.rb_parent = None  # radio buttons only: xref of owning parent

        self.field_name = None  # field name
        self.field_label = None  # field label
        self.field_value = None
        self.field_flags = 0
        self.field_display = 0
        self.field_type = 0  # valid range 1 through 7
        self.field_type_string = None  # field type as string

        self.fill_color = None
        self.button_caption = None  # button caption
        self.is_signed = None  # True / False if signature
        self.text_color = (0, 0, 0)
        self.text_font = "Helv"
        self.text_fontsize = 0
        self.text_maxlen = 0  # text fields only
        self.text_format = 0  # text fields only
        self._text_da = ""  # /DA = default apparance

        self.script = None  # JavaScript (/A)
        self.script_stroke = None  # JavaScript (/AA/K)
        self.script_format = None  # JavaScript (/AA/F)
        self.script_change = None  # JavaScript (/AA/V)
        self.script_calc = None  # JavaScript (/AA/C)
        self.script_blur = None  # JavaScript (/AA/Bl)
        self.script_focus = None  # JavaScript (/AA/Fo)

        self.rect = None  # annot value
        self.xref = 0  # annot value


    def _validate(self):
        """Validate the class entries.
        """
        if (self.rect.is_infinite
            or self.rect.is_empty
           ):
            raise ValueError("bad rect")

        if not self.field_name:
            raise ValueError("field name missing")

        if self.field_label == "Unnamed":
            self.field_label = None
        CheckColor(self.border_color)
        CheckColor(self.fill_color)
        if not self.text_color:
            self.text_color = (0, 0, 0)
        CheckColor(self.text_color)

        if not self.border_width:
            self.border_width = 0

        if not self.text_fontsize:
            self.text_fontsize = 0

        self.border_style = self.border_style.upper()[0:1]

        # standardize content of JavaScript entries
        btn_type = self.field_type in (
            PDF_WIDGET_TYPE_BUTTON,
            PDF_WIDGET_TYPE_CHECKBOX,
            PDF_WIDGET_TYPE_RADIOBUTTON
        )
        if not self.script:
            self.script = None
        elif type(self.script) is not str:
            raise ValueError("script content must be a string")

        # buttons cannot have the following script actions
        if btn_type or not self.script_calc:
            self.script_calc = None
        elif type(self.script_calc) is not str:
            raise ValueError("script_calc content must be a string")

        if btn_type or not self.script_change:
            self.script_change = None
        elif type(self.script_change) is not str:
            raise ValueError("script_change content must be a string")

        if btn_type or not self.script_format:
            self.script_format = None
        elif type(self.script_format) is not str:
            raise ValueError("script_format content must be a string")

        if btn_type or not self.script_stroke:
            self.script_stroke = None
        elif type(self.script_stroke) is not str:
            raise ValueError("script_stroke content must be a string")

        if btn_type or not self.script_blur:
            self.script_blur = None
        elif type(self.script_blur) is not str:
            raise ValueError("script_blur content must be a string")

        if btn_type or not self.script_focus:
            self.script_focus = None
        elif type(self.script_focus) is not str:
            raise ValueError("script_focus content must be a string")

        self._checker()  # any field_type specific checks


    def _adjust_font(self):
        """Ensure text_font is correctly spelled if empty or from our list.

        Otherwise assume the font is in an existing field.
        """
        if not self.text_font:
            self.text_font = "Helv"
            return
        doc = self.parent.parent
        for f in doc.FormFonts + ["Cour", "TiRo", "Helv", "ZaDb"]:
            if self.text_font.lower() == f.lower():
                self.text_font = f
                return
        self.text_font = "Helv"
        return


    def _parse_da(self):
        """Extract font name, size and color from default appearance string (/DA object).

        Equivalent to 'pdf_parse_default_appearance' function in MuPDF's 'pdf-annot.c'.
        """
        if not self._text_da:
            return
        font = "Helv"
        fsize = 0
        col = (0, 0, 0)
        dat = self._text_da.split()  # split on any whitespace
        for i, item in enumerate(dat):
            if item == "Tf":
                font = dat[i - 2][1:]
                fsize = float(dat[i - 1])
                dat[i] = dat[i-1] = dat[i-2] = ""
                continue
            if item == "g":  # unicolor text
                col = [(float(dat[i - 1]))]
                dat[i] = dat[i-1] = ""
                continue
            if item == "rg":  # RGB colored text
                col = [float(f) for f in dat[i - 3:i]]
                dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = ""
                continue
        self.text_font = font
        self.text_fontsize = fsize
        self.text_color = col
        self._text_da = ""
        return


    def _checker(self):
        """Any widget type checks.
        """
        if self.field_type not in range(1, 8):
            raise ValueError("bad field type")


        # if setting a radio button to ON, first set Off all buttons
        # in the group - this is not done by MuPDF:
        if self.field_type == PDF_WIDGET_TYPE_RADIOBUTTON and self.field_value not in (False, "Off") and hasattr(self, "parent"):
            # so we are about setting this button to ON/True
            # check other buttons in same group and set them to 'Off'
            doc = self.parent.parent
            kids_type, kids_value = doc.xref_get_key(self.xref, "Parent/Kids")
            if kids_type == "array":
                xrefs = tuple(map(int, kids_value[1:-1].replace("0 R","").split()))
                for xref in xrefs:
                    if xref != self.xref:
                        doc.xref_set_key(xref, "AS", "/Off")
        # the calling method will now set the intended button to on and
        # will find everything prepared for correct functioning.


    def update(self):
        """Reflect Python object in the PDF.
        """
        doc = self.parent.parent
        self._validate()

        self._adjust_font()  # ensure valid text_font name

        # now create the /DA string
        self._text_da = ""
        if   len(self.text_color) == 3:
            fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf" + self._text_da
        elif len(self.text_color) == 1:
            fmt = "{:g} g /{f:s} {s:g} Tf" + self._text_da
        elif len(self.text_color) == 4:
            fmt = "{:g} {:g} {:g} {:g} k /{f:s} {s:g} Tf" + self._text_da
        self._text_da = fmt.format(*self.text_color, f=self.text_font,
                                    s=self.text_fontsize)

        # if widget has a '/AA/C' script, make sure it is in the '/CO'
        # array of the '/AcroForm' dictionary.
        if self.script_calc:  # there is a "calculation" script:
            # make sure we are in the /CO array
            util_ensure_widget_calc(self._annot)

        # finally update the widget
        TOOLS._save_widget(self._annot, self)
        self._text_da = ""


    def button_states(self):
        """Return the on/off state names for button widgets.

        A button may have 'normal' or 'pressed down' appearances. While the 'Off'
        state is usually called like this, the 'On' state is often given a name
        relating to the functional context.
        """
        if self.field_type not in (2, 5):
            return None  # no button type
        if hasattr(self, "parent"):  # field already exists on page
            doc = self.parent.parent
        else:
            return None
        xref = self.xref
        states = {"normal": None, "down": None}
        APN = doc.xref_get_key(xref, "AP/N")
        if APN[0] == "dict":
            nstates = []
            APN = APN[1][2:-2]
            apnt = APN.split("/")[1:]
            for x in apnt:
                nstates.append(x.split()[0])
            states["normal"] = nstates
        if APN[0] == "xref":
            nstates = []
            nxref = int(APN[1].split(" ")[0])
            APN = doc.xref_object(nxref)
            apnt = APN.split("/")[1:]
            for x in apnt:
                nstates.append(x.split()[0])
            states["normal"] = nstates
        APD = doc.xref_get_key(xref, "AP/D")
        if APD[0] == "dict":
            dstates = []
            APD = APD[1][2:-2]
            apdt = APD.split("/")[1:]
            for x in apdt:
                dstates.append(x.split()[0])
            states["down"] = dstates
        if APD[0] == "xref":
            dstates = []
            dxref = int(APD[1].split(" ")[0])
            APD = doc.xref_object(dxref)
            apdt = APD.split("/")[1:]
            for x in apdt:
                dstates.append(x.split()[0])
            states["down"] = dstates
        return states

    def on_state(self):
        """Return the "On" value for button widgets.

        This is useful for radio buttons mainly. Checkboxes will always return
        "Yes". Radio buttons will return the string that is unequal to "Off"
        as returned by method button_states().
        If the radio button is new / being created, it does not yet have an
        "On" value. In this case, a warning is shown and True is returned.
        """
        if self.field_type not in (2, 5):
            return None  # no checkbox or radio button
        if self.field_type == 2:
            return "Yes"
        bstate = self.button_states()
        if bstate==None:
            bstate = {}
        for k in bstate.keys():
            for v in bstate[k]:
                if v != "Off":
                    return v
        print("warning: radio button has no 'On' value.")
        return True

    def reset(self):
        """Reset the field value to its default.
        """
        TOOLS._reset_widget(self._annot)

    def __repr__(self):
        return "'%s' widget on %s" % (self.field_type_string, str(self.parent))

    def __del__(self):
        if hasattr(self, "_annot"):
            del self._annot

    @property
    def next(self):
        return self._annot.next
%}