comparison 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
comparison
equal deleted inserted replaced
0:6015a75abc2d 3:2c135c81b16c
1 %{
2 /*
3 # ------------------------------------------------------------------------
4 # Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com
5 # License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html
6 #
7 # Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a
8 # lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is
9 # maintained and developed by Artifex Software, Inc. https://artifex.com.
10 # ------------------------------------------------------------------------
11 */
12 #define SETATTR(a, v) PyObject_SetAttrString(Widget, a, v)
13 #define GETATTR(a) PyObject_GetAttrString(Widget, a)
14 #define CALLATTR(m, p) PyObject_CallMethod(Widget, m, p)
15
16 static void
17 SETATTR_DROP(PyObject *mod, const char *attr, PyObject *value)
18 {
19 if (!value)
20 PyObject_DelAttrString(mod, attr);
21 else
22 {
23 PyObject_SetAttrString(mod, attr, value);
24 Py_DECREF(value);
25 }
26 }
27
28 //-----------------------------------------------------------------------------
29 // Functions dealing with PDF form fields (widgets)
30 //-----------------------------------------------------------------------------
31 enum
32 {
33 SigFlag_SignaturesExist = 1,
34 SigFlag_AppendOnly = 2
35 };
36
37
38 // make new PDF action object from JavaScript source
39 // Parameters are a PDF document and a Python string.
40 // Returns a PDF action object.
41 //-----------------------------------------------------------------------------
42 pdf_obj *
43 JM_new_javascript(fz_context *ctx, pdf_document *pdf, PyObject *value)
44 {
45 fz_buffer *res = NULL;
46 if (!PyObject_IsTrue(value)) // no argument given
47 return NULL;
48
49 char *data = JM_StrAsChar(value);
50 if (!data) // not convertible to char*
51 return NULL;
52
53 res = fz_new_buffer_from_copied_data(ctx, data, strlen(data));
54 pdf_obj *source = pdf_add_stream(ctx, pdf, res, NULL, 0);
55 pdf_obj *newaction = pdf_add_new_dict(ctx, pdf, 4);
56 pdf_dict_put(ctx, newaction, PDF_NAME(S), pdf_new_name(ctx, "JavaScript"));
57 pdf_dict_put(ctx, newaction, PDF_NAME(JS), source);
58 fz_drop_buffer(ctx, res);
59 return pdf_keep_obj(ctx, newaction);
60 }
61
62
63 // JavaScript extractor
64 // Returns either the script source or None. Parameter is a PDF action
65 // dictionary, which must have keys /S and /JS. The value of /S must be
66 // '/JavaScript'. The value of /JS is returned.
67 //-----------------------------------------------------------------------------
68 PyObject *
69 JM_get_script(fz_context *ctx, pdf_obj *key)
70 {
71 pdf_obj *js = NULL;
72 fz_buffer *res = NULL;
73 PyObject *script = NULL;
74 if (!key) Py_RETURN_NONE;
75
76 if (!strcmp(pdf_to_name(ctx,
77 pdf_dict_get(ctx, key, PDF_NAME(S))), "JavaScript")) {
78 js = pdf_dict_get(ctx, key, PDF_NAME(JS));
79 }
80 if (!js) Py_RETURN_NONE;
81
82 if (pdf_is_string(ctx, js)) {
83 script = JM_UnicodeFromStr(pdf_to_text_string(ctx, js));
84 } else if (pdf_is_stream(ctx, js)) {
85 res = pdf_load_stream(ctx, js);
86 script = JM_EscapeStrFromBuffer(ctx, res);
87 fz_drop_buffer(ctx, res);
88 } else {
89 Py_RETURN_NONE;
90 }
91 if (PyObject_IsTrue(script)) { // do not return an empty script
92 return script;
93 }
94 Py_CLEAR(script);
95 Py_RETURN_NONE;
96 }
97
98
99 // Create a JavaScript PDF action.
100 // Usable for all object types which support PDF actions, even if the
101 // argument name suggests annotations. Up to 2 key values can be specified, so
102 // JavaScript actions can be stored for '/A' and '/AA/?' keys.
103 //-----------------------------------------------------------------------------
104 void JM_put_script(fz_context *ctx, pdf_obj *annot_obj, pdf_obj *key1, pdf_obj *key2, PyObject *value)
105 {
106 PyObject *script = NULL;
107 pdf_obj *key1_obj = pdf_dict_get(ctx, annot_obj, key1);
108 pdf_document *pdf = pdf_get_bound_document(ctx, annot_obj); // owning PDF
109
110 // if no new script given, just delete corresponding key
111 if (!value || !PyObject_IsTrue(value)) {
112 if (!key2) {
113 pdf_dict_del(ctx, annot_obj, key1);
114 } else if (key1_obj) {
115 pdf_dict_del(ctx, key1_obj, key2);
116 }
117 return;
118 }
119
120 // read any existing script as a PyUnicode string
121 if (!key2 || !key1_obj) {
122 script = JM_get_script(ctx, key1_obj);
123 } else {
124 script = JM_get_script(ctx, pdf_dict_get(ctx, key1_obj, key2));
125 }
126
127 // replace old script, if different from new one
128 if (!PyObject_RichCompareBool(value, script, Py_EQ)) {
129 pdf_obj *newaction = JM_new_javascript(ctx, pdf, value);
130 if (!key2) {
131 pdf_dict_put_drop(ctx, annot_obj, key1, newaction);
132 } else {
133 pdf_dict_putl_drop(ctx, annot_obj, newaction, key1, key2, NULL);
134 }
135 }
136 Py_XDECREF(script);
137 return;
138 }
139
140 /*
141 // Execute a JavaScript action for annot or field.
142 //-----------------------------------------------------------------------------
143 PyObject *
144 JM_exec_script(fz_context *ctx, pdf_obj *annot_obj, pdf_obj *key1, pdf_obj *key2)
145 {
146 PyObject *script = NULL;
147 char *code = NULL;
148 fz_try(ctx) {
149 pdf_document *pdf = pdf_get_bound_document(ctx, annot_obj);
150 char buf[100];
151 if (!key2) {
152 script = JM_get_script(ctx, key1_obj);
153 } else {
154 script = JM_get_script(ctx, pdf_dict_get(ctx, key1_obj, key2));
155 }
156 code = JM_StrAsChar(script);
157 fz_snprintf(buf, sizeof buf, "%d/A", pdf_to_num(ctx, annot_obj));
158 pdf_js_execute(pdf->js, buf, code);
159 }
160 fz_always(ctx) {
161 Py_XDECREF(string);
162 }
163 fz_catch(ctx) {
164 Py_RETURN_FALSE;
165 }
166 Py_RETURN_TRUE;
167 }
168 */
169
170 // String from widget type
171 //-----------------------------------------------------------------------------
172 char *JM_field_type_text(int wtype)
173 {
174 switch(wtype) {
175 case(PDF_WIDGET_TYPE_BUTTON):
176 return "Button";
177 case(PDF_WIDGET_TYPE_CHECKBOX):
178 return "CheckBox";
179 case(PDF_WIDGET_TYPE_RADIOBUTTON):
180 return "RadioButton";
181 case(PDF_WIDGET_TYPE_TEXT):
182 return "Text";
183 case(PDF_WIDGET_TYPE_LISTBOX):
184 return "ListBox";
185 case(PDF_WIDGET_TYPE_COMBOBOX):
186 return "ComboBox";
187 case(PDF_WIDGET_TYPE_SIGNATURE):
188 return "Signature";
189 default:
190 return "unknown";
191 }
192 }
193
194 // Set the field type
195 //-----------------------------------------------------------------------------
196 void JM_set_field_type(fz_context *ctx, pdf_document *doc, pdf_obj *obj, int type)
197 {
198 int setbits = 0;
199 int clearbits = 0;
200 pdf_obj *typename = NULL;
201
202 switch(type) {
203 case PDF_WIDGET_TYPE_BUTTON:
204 typename = PDF_NAME(Btn);
205 setbits = PDF_BTN_FIELD_IS_PUSHBUTTON;
206 break;
207 case PDF_WIDGET_TYPE_RADIOBUTTON:
208 typename = PDF_NAME(Btn);
209 clearbits = PDF_BTN_FIELD_IS_PUSHBUTTON;
210 setbits = PDF_BTN_FIELD_IS_RADIO;
211 break;
212 case PDF_WIDGET_TYPE_CHECKBOX:
213 typename = PDF_NAME(Btn);
214 clearbits = (PDF_BTN_FIELD_IS_PUSHBUTTON|PDF_BTN_FIELD_IS_RADIO);
215 break;
216 case PDF_WIDGET_TYPE_TEXT:
217 typename = PDF_NAME(Tx);
218 break;
219 case PDF_WIDGET_TYPE_LISTBOX:
220 typename = PDF_NAME(Ch);
221 clearbits = PDF_CH_FIELD_IS_COMBO;
222 break;
223 case PDF_WIDGET_TYPE_COMBOBOX:
224 typename = PDF_NAME(Ch);
225 setbits = PDF_CH_FIELD_IS_COMBO;
226 break;
227 case PDF_WIDGET_TYPE_SIGNATURE:
228 typename = PDF_NAME(Sig);
229 break;
230 }
231
232 if (typename)
233 pdf_dict_put_drop(ctx, obj, PDF_NAME(FT), typename);
234
235 if (setbits != 0 || clearbits != 0) {
236 int bits = pdf_dict_get_int(ctx, obj, PDF_NAME(Ff));
237 bits &= ~clearbits;
238 bits |= setbits;
239 pdf_dict_put_int(ctx, obj, PDF_NAME(Ff), bits);
240 }
241 }
242
243 // Copied from MuPDF v1.14
244 // Create widget.
245 // Returns a kept reference to a pdf_annot - caller must drop it.
246 //-----------------------------------------------------------------------------
247 pdf_annot *JM_create_widget(fz_context *ctx, pdf_document *doc, pdf_page *page, int type, char *fieldname)
248 {
249 pdf_obj *form = NULL;
250 int old_sigflags = pdf_to_int(ctx, pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/SigFlags"));
251 pdf_annot *annot = pdf_create_annot_raw(ctx, page, PDF_ANNOT_WIDGET); // returns a kept reference.
252 pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
253 fz_try(ctx) {
254 JM_set_field_type(ctx, doc, annot_obj, type);
255 pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(T), fieldname);
256
257 if (type == PDF_WIDGET_TYPE_SIGNATURE) {
258 int sigflags = (old_sigflags | (SigFlag_SignaturesExist|SigFlag_AppendOnly));
259 pdf_dict_putl_drop(ctx, pdf_trailer(ctx, doc), pdf_new_int(ctx, sigflags), PDF_NAME(Root), PDF_NAME(AcroForm), PDF_NAME(SigFlags), NULL);
260 }
261
262 /*
263 pdf_create_annot will have linked the new widget into the page's
264 annot array. We also need it linked into the document's form
265 */
266 form = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/Fields");
267 if (!form) {
268 form = pdf_new_array(ctx, doc, 1);
269 pdf_dict_putl_drop(ctx, pdf_trailer(ctx, doc),
270 form,
271 PDF_NAME(Root),
272 PDF_NAME(AcroForm),
273 PDF_NAME(Fields),
274 NULL);
275 }
276
277 pdf_array_push(ctx, form, annot_obj); // Cleanup relies on this statement being last
278 }
279 fz_catch(ctx) {
280 pdf_delete_annot(ctx, page, annot);
281
282 if (type == PDF_WIDGET_TYPE_SIGNATURE) {
283 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);
284 }
285
286 fz_rethrow(ctx);
287 }
288
289 return annot;
290 }
291
292
293
294 // PushButton get state
295 //-----------------------------------------------------------------------------
296 PyObject *JM_pushbtn_state(fz_context *ctx, pdf_annot *annot)
297 { // pushed buttons do not reflect status changes in the PDF
298 // always reflect them as untouched
299 Py_RETURN_FALSE;
300 }
301
302
303 // Text field retrieve value
304 //-----------------------------------------------------------------------------
305 PyObject *JM_text_value(fz_context *ctx, pdf_annot *annot)
306 {
307 const char *text = NULL;
308 fz_var(text);
309 fz_try(ctx) {
310 pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
311 text = pdf_field_value(ctx, annot_obj);
312 }
313 fz_catch(ctx) Py_RETURN_NONE;
314 return JM_UnicodeFromStr(text);
315 }
316
317 // ListBox retrieve value
318 //-----------------------------------------------------------------------------
319 PyObject *JM_listbox_value(fz_context *ctx, pdf_annot *annot)
320 {
321 int i = 0, n = 0;
322 // may be single value or array
323 pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
324 pdf_obj *optarr = pdf_dict_get(ctx, annot_obj, PDF_NAME(V));
325 if (pdf_is_string(ctx, optarr)) // a single string
326 return PyString_FromString(pdf_to_text_string(ctx, optarr));
327
328 // value is an array (may have len 0)
329 n = pdf_array_len(ctx, optarr);
330 PyObject *liste = PyList_New(0);
331
332 // extract a list of strings
333 // each entry may again be an array: take second entry then
334 for (i = 0; i < n; i++) {
335 pdf_obj *elem = pdf_array_get(ctx, optarr, i);
336 if (pdf_is_array(ctx, elem))
337 elem = pdf_array_get(ctx, elem, 1);
338 LIST_APPEND_DROP(liste, JM_UnicodeFromStr(pdf_to_text_string(ctx, elem)));
339 }
340 return liste;
341 }
342
343 // ComboBox retrieve value
344 //-----------------------------------------------------------------------------
345 PyObject *JM_combobox_value(fz_context *ctx, pdf_annot *annot)
346 { // combobox treated like listbox
347 return JM_listbox_value(ctx, annot);
348 }
349
350 // Signature field retrieve value
351 PyObject *JM_signature_value(fz_context *ctx, pdf_annot *annot)
352 { // signatures are currently not supported
353 Py_RETURN_NONE;
354 }
355
356 // retrieve ListBox / ComboBox choice values
357 //-----------------------------------------------------------------------------
358 PyObject *JM_choice_options(fz_context *ctx, pdf_annot *annot)
359 { // return list of choices for list or combo boxes
360 pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
361 PyObject *val;
362 int n = pdf_choice_widget_options(ctx, annot, 0, NULL);
363 if (n == 0) Py_RETURN_NONE; // wrong widget type
364
365 pdf_obj *optarr = pdf_dict_get(ctx, annot_obj, PDF_NAME(Opt));
366 int i, m;
367 PyObject *liste = PyList_New(0);
368
369 for (i = 0; i < n; i++) {
370 m = pdf_array_len(ctx, pdf_array_get(ctx, optarr, i));
371 if (m == 2) {
372 val = Py_BuildValue("ss",
373 pdf_to_text_string(ctx, pdf_array_get(ctx, pdf_array_get(ctx, optarr, i), 0)),
374 pdf_to_text_string(ctx, pdf_array_get(ctx, pdf_array_get(ctx, optarr, i), 1)));
375 LIST_APPEND_DROP(liste, val);
376 } else {
377 val = JM_UnicodeFromStr(pdf_to_text_string(ctx, pdf_array_get(ctx, optarr, i)));
378 LIST_APPEND_DROP(liste, val);
379 }
380 }
381 return liste;
382 }
383
384
385 // set ListBox / ComboBox values
386 //-----------------------------------------------------------------------------
387 void JM_set_choice_options(fz_context *ctx, pdf_annot *annot, PyObject *liste)
388 {
389 if (!liste) return;
390 if (!PySequence_Check(liste)) return;
391 Py_ssize_t i, n = PySequence_Size(liste);
392 if (n < 1) return;
393 PyObject *tuple = PySequence_Tuple(liste);
394 PyObject *val = NULL, *val1 = NULL, *val2 = NULL;
395 pdf_obj *optarrsub = NULL, *optarr = NULL, *annot_obj = NULL;
396 pdf_document *pdf = NULL;
397 const char *opt = NULL, *opt1 = NULL, *opt2 = NULL;
398 fz_try(ctx) {
399 annot_obj = pdf_annot_obj(ctx, annot);
400 pdf = pdf_get_bound_document(ctx, annot_obj);
401 optarr = pdf_new_array(ctx, pdf, (int) n);
402 for (i = 0; i < n; i++) {
403 val = PyTuple_GET_ITEM(tuple, i);
404 opt = PyUnicode_AsUTF8(val);
405 if (opt) {
406 pdf_array_push_text_string(ctx, optarr, opt);
407 } else {
408 if (!PySequence_Check(val) || PySequence_Size(val) != 2) {
409 RAISEPY(ctx, "bad choice field list", PyExc_ValueError);
410 }
411 val1 = PySequence_GetItem(val, 0);
412 opt1 = PyUnicode_AsUTF8(val1);
413 if (!opt1) {
414 RAISEPY(ctx, "bad choice field list", PyExc_ValueError);
415 }
416 val2 = PySequence_GetItem(val, 1);
417 opt2 = PyUnicode_AsUTF8(val2);
418 if (!opt2) {
419 RAISEPY(ctx, "bad choice field list", PyExc_ValueError);
420 };
421 Py_CLEAR(val1);
422 Py_CLEAR(val2);
423 optarrsub = pdf_array_push_array(ctx, optarr, 2);
424 pdf_array_push_text_string(ctx, optarrsub, opt1);
425 pdf_array_push_text_string(ctx, optarrsub, opt2);
426 }
427 }
428 pdf_dict_put_drop(ctx, annot_obj, PDF_NAME(Opt), optarr);
429 }
430 fz_always(ctx) {
431 Py_CLEAR(tuple);
432 Py_CLEAR(val1);
433 Py_CLEAR(val2);
434 PyErr_Clear();
435 }
436 fz_catch(ctx) {
437 fz_rethrow(ctx);
438 }
439 return;
440 }
441
442
443 //-----------------------------------------------------------------------------
444 // Populate a Python Widget object with the values from a PDF form field.
445 // Called by "Page.firstWidget" and "Widget.next".
446 //-----------------------------------------------------------------------------
447 void JM_get_widget_properties(fz_context *ctx, pdf_annot *annot, PyObject *Widget)
448 {
449 pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
450 pdf_page *page = pdf_annot_page(ctx, annot);
451 pdf_document *pdf = page->doc;
452 pdf_annot *tw = annot;
453 pdf_obj *obj = NULL;
454 Py_ssize_t i = 0, n = 0;
455 fz_try(ctx) {
456 int field_type = pdf_widget_type(ctx, tw);
457 SETATTR_DROP(Widget, "field_type", Py_BuildValue("i", field_type));
458 if (field_type == PDF_WIDGET_TYPE_SIGNATURE) {
459 if (pdf_signature_is_signed(ctx, pdf, annot_obj)) {
460 SETATTR("is_signed", Py_True);
461 } else {
462 SETATTR("is_signed", Py_False);
463 }
464 } else {
465 SETATTR("is_signed", Py_None);
466 }
467 SETATTR_DROP(Widget, "border_style",
468 JM_UnicodeFromStr(pdf_field_border_style(ctx, annot_obj)));
469 SETATTR_DROP(Widget, "field_type_string",
470 JM_UnicodeFromStr(JM_field_type_text(field_type)));
471
472 #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR <= 22
473 char *field_name = pdf_field_name(ctx, annot_obj);
474 #else
475 char *field_name = pdf_load_field_name(ctx, annot_obj);
476 #endif
477 SETATTR_DROP(Widget, "field_name", JM_UnicodeFromStr(field_name));
478 JM_Free(field_name);
479
480 const char *label = NULL;
481 obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(TU));
482 if (obj) label = pdf_to_text_string(ctx, obj);
483 SETATTR_DROP(Widget, "field_label", JM_UnicodeFromStr(label));
484
485 const char *fvalue = NULL;
486 if (field_type == PDF_WIDGET_TYPE_RADIOBUTTON) {
487 obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(Parent)); // owning RB group
488 if (obj) {
489 SETATTR_DROP(Widget, "rb_parent", Py_BuildValue("i", pdf_to_num(ctx, obj)));
490 }
491 obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(AS));
492 if (obj) {
493 fvalue = pdf_to_name(ctx, obj);
494 }
495 }
496 if (!fvalue) {
497 fvalue = pdf_field_value(ctx, annot_obj);
498 }
499 SETATTR_DROP(Widget, "field_value", JM_UnicodeFromStr(fvalue));
500
501 SETATTR_DROP(Widget, "field_display",
502 Py_BuildValue("i", pdf_field_display(ctx, annot_obj)));
503
504 float border_width = pdf_to_real(ctx, pdf_dict_getl(ctx, annot_obj,
505 PDF_NAME(BS), PDF_NAME(W), NULL));
506 if (border_width == 0) border_width = 1;
507 SETATTR_DROP(Widget, "border_width",
508 Py_BuildValue("f", border_width));
509
510 obj = pdf_dict_getl(ctx, annot_obj,
511 PDF_NAME(BS), PDF_NAME(D), NULL);
512 if (pdf_is_array(ctx, obj)) {
513 n = (Py_ssize_t) pdf_array_len(ctx, obj);
514 PyObject *d = PyList_New(n);
515 for (i = 0; i < n; i++) {
516 PyList_SET_ITEM(d, i, Py_BuildValue("i", pdf_to_int(ctx,
517 pdf_array_get(ctx, obj, (int) i))));
518 }
519 SETATTR_DROP(Widget, "border_dashes", d);
520 }
521
522 SETATTR_DROP(Widget, "text_maxlen",
523 Py_BuildValue("i", pdf_text_widget_max_len(ctx, tw)));
524
525 SETATTR_DROP(Widget, "text_format",
526 Py_BuildValue("i", pdf_text_widget_format(ctx, tw)));
527
528 obj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(MK), PDF_NAME(BG), NULL);
529 if (pdf_is_array(ctx, obj)) {
530 n = (Py_ssize_t) pdf_array_len(ctx, obj);
531 PyObject *col = PyList_New(n);
532 for (i = 0; i < n; i++) {
533 PyList_SET_ITEM(col, i, Py_BuildValue("f",
534 pdf_to_real(ctx, pdf_array_get(ctx, obj, (int) i))));
535 }
536 SETATTR_DROP(Widget, "fill_color", col);
537 }
538
539 obj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(MK), PDF_NAME(BC), NULL);
540 if (pdf_is_array(ctx, obj)) {
541 n = (Py_ssize_t) pdf_array_len(ctx, obj);
542 PyObject *col = PyList_New(n);
543 for (i = 0; i < n; i++) {
544 PyList_SET_ITEM(col, i, Py_BuildValue("f",
545 pdf_to_real(ctx, pdf_array_get(ctx, obj, (int) i))));
546 }
547 SETATTR_DROP(Widget, "border_color", col);
548 }
549
550 SETATTR_DROP(Widget, "choice_values", JM_choice_options(ctx, annot));
551
552 const char *da = pdf_to_text_string(ctx, pdf_dict_get_inheritable(ctx,
553 annot_obj, PDF_NAME(DA)));
554 SETATTR_DROP(Widget, "_text_da", JM_UnicodeFromStr(da));
555
556 obj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(MK), PDF_NAME(CA), NULL);
557 if (obj) {
558 SETATTR_DROP(Widget, "button_caption",
559 JM_UnicodeFromStr((char *)pdf_to_text_string(ctx, obj)));
560 }
561
562 SETATTR_DROP(Widget, "field_flags",
563 Py_BuildValue("i", pdf_field_flags(ctx, annot_obj)));
564
565 // call Py method to reconstruct text color, font name, size
566 PyObject *call = CALLATTR("_parse_da", NULL);
567 Py_XDECREF(call);
568
569 // extract JavaScript action texts
570 SETATTR_DROP(Widget, "script",
571 JM_get_script(ctx, pdf_dict_get(ctx, annot_obj, PDF_NAME(A))));
572
573 SETATTR_DROP(Widget, "script_stroke",
574 JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(K), NULL)));
575
576 SETATTR_DROP(Widget, "script_format",
577 JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(F), NULL)));
578
579 SETATTR_DROP(Widget, "script_change",
580 JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(V), NULL)));
581
582 SETATTR_DROP(Widget, "script_calc",
583 JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(C), NULL)));
584
585 SETATTR_DROP(Widget, "script_blur",
586 JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), pdf_new_name(ctx, "Bl"), NULL)));
587
588 SETATTR_DROP(Widget, "script_focus",
589 JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), pdf_new_name(ctx, "Fo"), NULL)));
590 }
591 fz_always(ctx) PyErr_Clear();
592 fz_catch(ctx) fz_rethrow(ctx);
593 return;
594 }
595
596
597 //-----------------------------------------------------------------------------
598 // Update the PDF form field with the properties from a Python Widget object.
599 // Called by "Page.addWidget" and "Annot.updateWidget".
600 //-----------------------------------------------------------------------------
601 void JM_set_widget_properties(fz_context *ctx, pdf_annot *annot, PyObject *Widget)
602 {
603 pdf_page *page = pdf_annot_page(ctx, annot);
604 pdf_obj *annot_obj = pdf_annot_obj(ctx, annot);
605 pdf_document *pdf = page->doc;
606 fz_rect rect;
607 pdf_obj *fill_col = NULL, *border_col = NULL;
608 pdf_obj *dashes = NULL;
609 Py_ssize_t i, n = 0;
610 int d;
611 PyObject *value = GETATTR("field_type");
612 int field_type = (int) PyInt_AsLong(value);
613 Py_DECREF(value);
614
615 // rectangle --------------------------------------------------------------
616 value = GETATTR("rect");
617 rect = JM_rect_from_py(value);
618 Py_XDECREF(value);
619 fz_matrix rot_mat = JM_rotate_page_matrix(ctx, page);
620 rect = fz_transform_rect(rect, rot_mat);
621 pdf_set_annot_rect(ctx, annot, rect);
622
623 // fill color -------------------------------------------------------------
624 value = GETATTR("fill_color");
625 if (value && PySequence_Check(value)) {
626 n = PySequence_Size(value);
627 fill_col = pdf_new_array(ctx, pdf, n);
628 double col = 0;
629 for (i = 0; i < n; i++) {
630 JM_FLOAT_ITEM(value, i, &col);
631 pdf_array_push_real(ctx, fill_col, col);
632 }
633 pdf_field_set_fill_color(ctx, annot_obj, fill_col);
634 pdf_drop_obj(ctx, fill_col);
635 }
636 Py_XDECREF(value);
637
638 // dashes -----------------------------------------------------------------
639 value = GETATTR("border_dashes");
640 if (value && PySequence_Check(value)) {
641 n = PySequence_Size(value);
642 dashes = pdf_new_array(ctx, pdf, n);
643 for (i = 0; i < n; i++) {
644 pdf_array_push_int(ctx, dashes,
645 (int64_t) PyInt_AsLong(PySequence_ITEM(value, i)));
646 }
647 pdf_dict_putl_drop(ctx, annot_obj, dashes,
648 PDF_NAME(BS),
649 PDF_NAME(D),
650 NULL);
651 }
652 Py_XDECREF(value);
653
654 // border color -----------------------------------------------------------
655 value = GETATTR("border_color");
656 if (value && PySequence_Check(value)) {
657 n = PySequence_Size(value);
658 border_col = pdf_new_array(ctx, pdf, n);
659 double col = 0;
660 for (i = 0; i < n; i++) {
661 JM_FLOAT_ITEM(value, i, &col);
662 pdf_array_push_real(ctx, border_col, col);
663 }
664 pdf_dict_putl_drop(ctx, annot_obj, border_col,
665 PDF_NAME(MK),
666 PDF_NAME(BC),
667 NULL);
668 }
669 Py_XDECREF(value);
670
671 // entry ignored - may be used later
672 /*
673 int text_format = (int) PyInt_AsLong(GETATTR("text_format"));
674 */
675
676 // field label -----------------------------------------------------------
677 value = GETATTR("field_label");
678 if (value != Py_None) {
679 char *label = JM_StrAsChar(value);
680 pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(TU), label);
681 }
682 Py_XDECREF(value);
683
684 // field name -------------------------------------------------------------
685 value = GETATTR("field_name");
686 if (value != Py_None) {
687 char *name = JM_StrAsChar(value);
688 #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR <= 22
689 char *old_name = pdf_field_name(ctx, annot_obj);
690 #else
691 char *old_name = pdf_load_field_name(ctx, annot_obj);
692 #endif
693 if (strcmp(name, old_name) != 0) {
694 pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(T), name);
695 }
696 JM_Free(old_name);
697 }
698 Py_XDECREF(value);
699
700 // max text len -----------------------------------------------------------
701 if (field_type == PDF_WIDGET_TYPE_TEXT)
702 {
703 value = GETATTR("text_maxlen");
704 int text_maxlen = (int) PyInt_AsLong(value);
705 if (text_maxlen) {
706 pdf_dict_put_int(ctx, annot_obj, PDF_NAME(MaxLen), text_maxlen);
707 }
708 Py_XDECREF(value);
709 }
710 value = GETATTR("field_display");
711 d = (int) PyInt_AsLong(value);
712 Py_XDECREF(value);
713 pdf_field_set_display(ctx, annot_obj, d);
714
715 // choice values ----------------------------------------------------------
716 if (field_type == PDF_WIDGET_TYPE_LISTBOX ||
717 field_type == PDF_WIDGET_TYPE_COMBOBOX) {
718 value = GETATTR("choice_values");
719 JM_set_choice_options(ctx, annot, value);
720 Py_XDECREF(value);
721 }
722
723 // border style -----------------------------------------------------------
724 value = GETATTR("border_style");
725 pdf_obj *val = JM_get_border_style(ctx, value);
726 Py_XDECREF(value);
727 pdf_dict_putl_drop(ctx, annot_obj, val,
728 PDF_NAME(BS),
729 PDF_NAME(S),
730 NULL);
731
732 // border width -----------------------------------------------------------
733 value = GETATTR("border_width");
734 float border_width = (float) PyFloat_AsDouble(value);
735 Py_XDECREF(value);
736 pdf_dict_putl_drop(ctx, annot_obj, pdf_new_real(ctx, border_width),
737 PDF_NAME(BS),
738 PDF_NAME(W),
739 NULL);
740
741 // /DA string -------------------------------------------------------------
742 value = GETATTR("_text_da");
743 char *da = JM_StrAsChar(value);
744 Py_XDECREF(value);
745 pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(DA), da);
746 pdf_dict_del(ctx, annot_obj, PDF_NAME(DS)); /* not supported by MuPDF */
747 pdf_dict_del(ctx, annot_obj, PDF_NAME(RC)); /* not supported by MuPDF */
748
749 // field flags ------------------------------------------------------------
750 value = GETATTR("field_flags");
751 int field_flags = (int) PyInt_AsLong(value);
752 Py_XDECREF(value);
753 if (!PyErr_Occurred()) {
754 if (field_type == PDF_WIDGET_TYPE_COMBOBOX) {
755 field_flags |= PDF_CH_FIELD_IS_COMBO;
756 } else if (field_type == PDF_WIDGET_TYPE_RADIOBUTTON) {
757 field_flags |= PDF_BTN_FIELD_IS_RADIO;
758 } else if (field_type == PDF_WIDGET_TYPE_BUTTON) {
759 field_flags |= PDF_BTN_FIELD_IS_PUSHBUTTON;
760 }
761 pdf_dict_put_int(ctx, annot_obj, PDF_NAME(Ff), field_flags);
762 }
763
764 // button caption ---------------------------------------------------------
765 value = GETATTR("button_caption");
766 char *ca = JM_StrAsChar(value);
767 if (ca) {
768 pdf_field_set_button_caption(ctx, annot_obj, ca);
769 }
770 Py_XDECREF(value);
771
772 // script (/A) -------------------------------------------------------
773 value = GETATTR("script");
774 JM_put_script(ctx, annot_obj, PDF_NAME(A), NULL, value);
775 Py_CLEAR(value);
776
777 // script (/AA/K) -------------------------------------------------------
778 value = GETATTR("script_stroke");
779 JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(K), value);
780 Py_CLEAR(value);
781
782 // script (/AA/F) -------------------------------------------------------
783 value = GETATTR("script_format");
784 JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(F), value);
785 Py_CLEAR(value);
786
787 // script (/AA/V) -------------------------------------------------------
788 value = GETATTR("script_change");
789 JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(V), value);
790 Py_CLEAR(value);
791
792 // script (/AA/C) -------------------------------------------------------
793 value = GETATTR("script_calc");
794 JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(C), value);
795 Py_CLEAR(value);
796
797 // script (/AA/Bl) ------------------------------------------------------
798 value = GETATTR("script_blur");
799 JM_put_script(ctx, annot_obj, PDF_NAME(AA), pdf_new_name(ctx, "Bl"), value);
800 Py_CLEAR(value);
801
802 // script (/AA/Fo) ------------------------------------------------------
803 value = GETATTR("script_focus");
804 JM_put_script(ctx, annot_obj, PDF_NAME(AA), pdf_new_name(ctx, "Fo"), value);
805 Py_CLEAR(value);
806
807 // field value ------------------------------------------------------------
808 value = GETATTR("field_value"); // field value
809 char *text = JM_StrAsChar(value); // convert to text (may fail!)
810
811 switch(field_type)
812 {
813 case PDF_WIDGET_TYPE_RADIOBUTTON:
814 if (PyObject_RichCompareBool(value, Py_False, Py_EQ)) {
815 pdf_set_field_value(ctx, pdf, annot_obj, "Off", 1);
816 pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), "Off");
817 } else {
818 pdf_obj *onstate = pdf_button_field_on_state(ctx, annot_obj);
819 if (onstate) {
820 const char *on = pdf_to_name(ctx, onstate);
821 pdf_set_field_value(ctx, pdf, annot_obj, on, 1);
822 pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), on);
823 } else if (text) {
824 pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), text);
825 }
826 }
827 break;
828
829 case PDF_WIDGET_TYPE_CHECKBOX: // will always be "Yes" or "Off"
830 if (PyObject_RichCompareBool(value, Py_True, Py_EQ) || text && strcmp(text, "Yes")==0) {
831 pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), "Yes");
832 pdf_dict_put_name(gctx, annot_obj, PDF_NAME(V), "Yes");
833 } else {
834 pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), "Off");
835 pdf_dict_put_name(gctx, annot_obj, PDF_NAME(V), "Off");
836 }
837 break;
838
839 default:
840 if (text) {
841 pdf_set_field_value(ctx, pdf, annot_obj, (const char *)text, 1);
842 if (field_type == PDF_WIDGET_TYPE_COMBOBOX || field_type == PDF_WIDGET_TYPE_LISTBOX) {
843 pdf_dict_del(ctx, annot_obj, PDF_NAME(I));
844 }
845 }
846 }
847 Py_CLEAR(value);
848 PyErr_Clear();
849 pdf_dirty_annot(ctx, annot);
850 pdf_set_annot_hot(ctx, annot, 1);
851 pdf_set_annot_active(ctx, annot, 1);
852 pdf_update_annot(ctx, annot);
853 }
854 #undef SETATTR
855 #undef GETATTR
856 #undef CALLATTR
857 %}
858
859 %pythoncode %{
860 #------------------------------------------------------------------------------
861 # Class describing a PDF form field ("widget")
862 #------------------------------------------------------------------------------
863 class Widget(object):
864 def __init__(self):
865 self.thisown = True
866 self.border_color = None
867 self.border_style = "S"
868 self.border_width = 0
869 self.border_dashes = None
870 self.choice_values = None # choice fields only
871 self.rb_parent = None # radio buttons only: xref of owning parent
872
873 self.field_name = None # field name
874 self.field_label = None # field label
875 self.field_value = None
876 self.field_flags = 0
877 self.field_display = 0
878 self.field_type = 0 # valid range 1 through 7
879 self.field_type_string = None # field type as string
880
881 self.fill_color = None
882 self.button_caption = None # button caption
883 self.is_signed = None # True / False if signature
884 self.text_color = (0, 0, 0)
885 self.text_font = "Helv"
886 self.text_fontsize = 0
887 self.text_maxlen = 0 # text fields only
888 self.text_format = 0 # text fields only
889 self._text_da = "" # /DA = default apparance
890
891 self.script = None # JavaScript (/A)
892 self.script_stroke = None # JavaScript (/AA/K)
893 self.script_format = None # JavaScript (/AA/F)
894 self.script_change = None # JavaScript (/AA/V)
895 self.script_calc = None # JavaScript (/AA/C)
896 self.script_blur = None # JavaScript (/AA/Bl)
897 self.script_focus = None # JavaScript (/AA/Fo)
898
899 self.rect = None # annot value
900 self.xref = 0 # annot value
901
902
903 def _validate(self):
904 """Validate the class entries.
905 """
906 if (self.rect.is_infinite
907 or self.rect.is_empty
908 ):
909 raise ValueError("bad rect")
910
911 if not self.field_name:
912 raise ValueError("field name missing")
913
914 if self.field_label == "Unnamed":
915 self.field_label = None
916 CheckColor(self.border_color)
917 CheckColor(self.fill_color)
918 if not self.text_color:
919 self.text_color = (0, 0, 0)
920 CheckColor(self.text_color)
921
922 if not self.border_width:
923 self.border_width = 0
924
925 if not self.text_fontsize:
926 self.text_fontsize = 0
927
928 self.border_style = self.border_style.upper()[0:1]
929
930 # standardize content of JavaScript entries
931 btn_type = self.field_type in (
932 PDF_WIDGET_TYPE_BUTTON,
933 PDF_WIDGET_TYPE_CHECKBOX,
934 PDF_WIDGET_TYPE_RADIOBUTTON
935 )
936 if not self.script:
937 self.script = None
938 elif type(self.script) is not str:
939 raise ValueError("script content must be a string")
940
941 # buttons cannot have the following script actions
942 if btn_type or not self.script_calc:
943 self.script_calc = None
944 elif type(self.script_calc) is not str:
945 raise ValueError("script_calc content must be a string")
946
947 if btn_type or not self.script_change:
948 self.script_change = None
949 elif type(self.script_change) is not str:
950 raise ValueError("script_change content must be a string")
951
952 if btn_type or not self.script_format:
953 self.script_format = None
954 elif type(self.script_format) is not str:
955 raise ValueError("script_format content must be a string")
956
957 if btn_type or not self.script_stroke:
958 self.script_stroke = None
959 elif type(self.script_stroke) is not str:
960 raise ValueError("script_stroke content must be a string")
961
962 if btn_type or not self.script_blur:
963 self.script_blur = None
964 elif type(self.script_blur) is not str:
965 raise ValueError("script_blur content must be a string")
966
967 if btn_type or not self.script_focus:
968 self.script_focus = None
969 elif type(self.script_focus) is not str:
970 raise ValueError("script_focus content must be a string")
971
972 self._checker() # any field_type specific checks
973
974
975 def _adjust_font(self):
976 """Ensure text_font is correctly spelled if empty or from our list.
977
978 Otherwise assume the font is in an existing field.
979 """
980 if not self.text_font:
981 self.text_font = "Helv"
982 return
983 doc = self.parent.parent
984 for f in doc.FormFonts + ["Cour", "TiRo", "Helv", "ZaDb"]:
985 if self.text_font.lower() == f.lower():
986 self.text_font = f
987 return
988 self.text_font = "Helv"
989 return
990
991
992 def _parse_da(self):
993 """Extract font name, size and color from default appearance string (/DA object).
994
995 Equivalent to 'pdf_parse_default_appearance' function in MuPDF's 'pdf-annot.c'.
996 """
997 if not self._text_da:
998 return
999 font = "Helv"
1000 fsize = 0
1001 col = (0, 0, 0)
1002 dat = self._text_da.split() # split on any whitespace
1003 for i, item in enumerate(dat):
1004 if item == "Tf":
1005 font = dat[i - 2][1:]
1006 fsize = float(dat[i - 1])
1007 dat[i] = dat[i-1] = dat[i-2] = ""
1008 continue
1009 if item == "g": # unicolor text
1010 col = [(float(dat[i - 1]))]
1011 dat[i] = dat[i-1] = ""
1012 continue
1013 if item == "rg": # RGB colored text
1014 col = [float(f) for f in dat[i - 3:i]]
1015 dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = ""
1016 continue
1017 self.text_font = font
1018 self.text_fontsize = fsize
1019 self.text_color = col
1020 self._text_da = ""
1021 return
1022
1023
1024 def _checker(self):
1025 """Any widget type checks.
1026 """
1027 if self.field_type not in range(1, 8):
1028 raise ValueError("bad field type")
1029
1030
1031 # if setting a radio button to ON, first set Off all buttons
1032 # in the group - this is not done by MuPDF:
1033 if self.field_type == PDF_WIDGET_TYPE_RADIOBUTTON and self.field_value not in (False, "Off") and hasattr(self, "parent"):
1034 # so we are about setting this button to ON/True
1035 # check other buttons in same group and set them to 'Off'
1036 doc = self.parent.parent
1037 kids_type, kids_value = doc.xref_get_key(self.xref, "Parent/Kids")
1038 if kids_type == "array":
1039 xrefs = tuple(map(int, kids_value[1:-1].replace("0 R","").split()))
1040 for xref in xrefs:
1041 if xref != self.xref:
1042 doc.xref_set_key(xref, "AS", "/Off")
1043 # the calling method will now set the intended button to on and
1044 # will find everything prepared for correct functioning.
1045
1046
1047 def update(self):
1048 """Reflect Python object in the PDF.
1049 """
1050 doc = self.parent.parent
1051 self._validate()
1052
1053 self._adjust_font() # ensure valid text_font name
1054
1055 # now create the /DA string
1056 self._text_da = ""
1057 if len(self.text_color) == 3:
1058 fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf" + self._text_da
1059 elif len(self.text_color) == 1:
1060 fmt = "{:g} g /{f:s} {s:g} Tf" + self._text_da
1061 elif len(self.text_color) == 4:
1062 fmt = "{:g} {:g} {:g} {:g} k /{f:s} {s:g} Tf" + self._text_da
1063 self._text_da = fmt.format(*self.text_color, f=self.text_font,
1064 s=self.text_fontsize)
1065
1066 # if widget has a '/AA/C' script, make sure it is in the '/CO'
1067 # array of the '/AcroForm' dictionary.
1068 if self.script_calc: # there is a "calculation" script:
1069 # make sure we are in the /CO array
1070 util_ensure_widget_calc(self._annot)
1071
1072 # finally update the widget
1073 TOOLS._save_widget(self._annot, self)
1074 self._text_da = ""
1075
1076
1077 def button_states(self):
1078 """Return the on/off state names for button widgets.
1079
1080 A button may have 'normal' or 'pressed down' appearances. While the 'Off'
1081 state is usually called like this, the 'On' state is often given a name
1082 relating to the functional context.
1083 """
1084 if self.field_type not in (2, 5):
1085 return None # no button type
1086 if hasattr(self, "parent"): # field already exists on page
1087 doc = self.parent.parent
1088 else:
1089 return None
1090 xref = self.xref
1091 states = {"normal": None, "down": None}
1092 APN = doc.xref_get_key(xref, "AP/N")
1093 if APN[0] == "dict":
1094 nstates = []
1095 APN = APN[1][2:-2]
1096 apnt = APN.split("/")[1:]
1097 for x in apnt:
1098 nstates.append(x.split()[0])
1099 states["normal"] = nstates
1100 if APN[0] == "xref":
1101 nstates = []
1102 nxref = int(APN[1].split(" ")[0])
1103 APN = doc.xref_object(nxref)
1104 apnt = APN.split("/")[1:]
1105 for x in apnt:
1106 nstates.append(x.split()[0])
1107 states["normal"] = nstates
1108 APD = doc.xref_get_key(xref, "AP/D")
1109 if APD[0] == "dict":
1110 dstates = []
1111 APD = APD[1][2:-2]
1112 apdt = APD.split("/")[1:]
1113 for x in apdt:
1114 dstates.append(x.split()[0])
1115 states["down"] = dstates
1116 if APD[0] == "xref":
1117 dstates = []
1118 dxref = int(APD[1].split(" ")[0])
1119 APD = doc.xref_object(dxref)
1120 apdt = APD.split("/")[1:]
1121 for x in apdt:
1122 dstates.append(x.split()[0])
1123 states["down"] = dstates
1124 return states
1125
1126 def on_state(self):
1127 """Return the "On" value for button widgets.
1128
1129 This is useful for radio buttons mainly. Checkboxes will always return
1130 "Yes". Radio buttons will return the string that is unequal to "Off"
1131 as returned by method button_states().
1132 If the radio button is new / being created, it does not yet have an
1133 "On" value. In this case, a warning is shown and True is returned.
1134 """
1135 if self.field_type not in (2, 5):
1136 return None # no checkbox or radio button
1137 if self.field_type == 2:
1138 return "Yes"
1139 bstate = self.button_states()
1140 if bstate==None:
1141 bstate = {}
1142 for k in bstate.keys():
1143 for v in bstate[k]:
1144 if v != "Off":
1145 return v
1146 print("warning: radio button has no 'On' value.")
1147 return True
1148
1149 def reset(self):
1150 """Reset the field value to its default.
1151 """
1152 TOOLS._reset_widget(self._annot)
1153
1154 def __repr__(self):
1155 return "'%s' widget on %s" % (self.field_type_string, str(self.parent))
1156
1157 def __del__(self):
1158 if hasattr(self, "_annot"):
1159 del self._annot
1160
1161 @property
1162 def next(self):
1163 return self._annot.next
1164 %}