Mercurial > hgrepos > Python2 > PyMuPDF
diff src_classic/helper-fields.i @ 3:2c135c81b16c
MERGE: upstream PyMuPDF 1.26.4 with MuPDF 1.26.7
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Mon, 15 Sep 2025 11:44:09 +0200 |
| parents | 1d09e1dec1d9 |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src_classic/helper-fields.i Mon Sep 15 11:44:09 2025 +0200 @@ -0,0 +1,1164 @@ +%{ +/* +# ------------------------------------------------------------------------ +# 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 +%}
