pymupdf / PyMuPDF

PyMuPDF is a high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents.
https://pymupdf.readthedocs.io
GNU Affero General Public License v3.0
5.33k stars 509 forks source link

Adding a button with specific Javascript (Customized) that can run in a PDF #454

Closed kurokawaikki closed 4 years ago

kurokawaikki commented 4 years ago

Can I add a button that can run Javascript? I mean... Can I use PyMuPDF to add a button widget that contain a specific Javascript? After exporting as PDF file, I can click on that button and run the Javascript that I predefined in PyMuPDF. Thank you very much for your great help!

JorjMcKie commented 4 years ago

Thanks for the submission. I will check this out. What I know so far: JavaScripts can be activated by form fields (e.g. buttons), but also by any annotation. Activating the script can be bound to certain events, which differ a little bit between different object types (pressing the mouse button, or releasing it, hovering over the object, etc.).

kurokawaikki commented 4 years ago

So...... Can I use the pyMUPDF to create a button (form fields) bound to specific javascript? What I know so far is that, pyMuPDF can create button by widget. But how can I bound the javascript to it? Thank you!!

JorjMcKie commented 4 years ago

I have investigated this and will publish this option in the next version. The button definition as PDF object will need to have an action entry /A which points to an action object roughly looking like this (at least for shorter scripts):

nnn 0 obj
<<
/S /JavaScript
/JS(... text of the script ...)
>>
endobj
JorjMcKie commented 4 years ago

I have developed and tested adding JavaScript to form fields. For your info, here is a first draft of the respective documentation:

New Widget Class properties:

script

(New in version 1.16.12) JavaScript code for an action associated with the widget, or None.

script_stroke

(New in version 1.16.12) JavaScript code to be performed when the user types a key-stroke into a text field or combo box or modifies the selection in a scrollable list box. This action can check the keystroke for validity and reject or modify it. Noneif not present.

script_format

(New in version 1.16.12) JavaScript code to be performed before the field is formatted to display its current value. This action can modify the field’s value before formatting. Noneif not present.

script_change

(New in version 1.16.12) JavaScript code to be performed when the field’s value is changed. This action can check the new value for validity. (The name V stands for“validate.”). Noneif not present.

script_calc

(New in version 1.16.12) JavaScript code to be performed to recalculate the value of this field when that of another field changes. (The name C stands for “calculate.”). Noneif not present.

Note For adding or changing one of the above scripts, just put the appropriate JavaScript code in the widget attribute. It will automatically replace the script previously stored in the PDF. To remove a script, set the respective attribute to None.

JorjMcKie commented 4 years ago

Please let me know if you would like an unofficial pre-version for testing. In that case I can send you a wheel via mail or this channel.

kurokawaikki commented 4 years ago

Thank you very much! I would very like to test the unofficial pre-version. I will try out the function to add Javascript to form filed and buttons when clicked. Can you provide the wheel in here? Thank you again for your great help.

JorjMcKie commented 4 years ago

Ok, right then. This is the list of wheels I have, please tell me which one you need:

PyMuPDF-1.16.12-cp27-cp27m-macosx_10_6_intel.whl
PyMuPDF-1.16.12-cp27-cp27m-manylinux2010_x86_64.whl
PyMuPDF-1.16.12-cp27-cp27m-win32.whl
PyMuPDF-1.16.12-cp27-cp27m-win_amd64.whl
PyMuPDF-1.16.12-cp27-cp27mu-manylinux2010_x86_64.whl
PyMuPDF-1.16.12-cp35-cp35m-macosx_10_6_intel.whl
PyMuPDF-1.16.12-cp35-cp35m-manylinux2010_x86_64.whl
PyMuPDF-1.16.12-cp35-cp35m-win32.whl
PyMuPDF-1.16.12-cp35-cp35m-win_amd64.whl
PyMuPDF-1.16.12-cp36-cp36m-macosx_10_6_intel.whl
PyMuPDF-1.16.12-cp36-cp36m-manylinux2010_x86_64.whl
PyMuPDF-1.16.12-cp36-cp36m-win32.whl
PyMuPDF-1.16.12-cp36-cp36m-win_amd64.whl
PyMuPDF-1.16.12-cp37-cp37m-macosx_10_6_intel.whl
PyMuPDF-1.16.12-cp37-cp37m-manylinux2010_x86_64.whl
PyMuPDF-1.16.12-cp37-cp37m-win32.whl
PyMuPDF-1.16.12-cp37-cp37m-win_amd64.whl
PyMuPDF-1.16.12-cp38-cp38-macosx_10_9_x86_64.whl
PyMuPDF-1.16.12-cp38-cp38-manylinux2010_x86_64.whl
PyMuPDF-1.16.12-cp38-cp38-win32.whl
PyMuPDF-1.16.12-cp38-cp38-win_amd64.whl
kurokawaikki commented 4 years ago

May I have "PyMuPDF-1.16.12-cp38-cp38-win_amd64.whl"? Thank you very much!

JorjMcKie commented 4 years ago

PyMuPDF-1.16.12-cp38-cp38-win_amd64.zip

Github won't let me upload *.whl files. So you have to change the extension from .zip to .whl before installing. Good luck!

kurokawaikki commented 4 years ago

Thank you! I have installed it. I tried to create the widget with your instructions.

doc = fitz.open("TEST.pdf")
p = doc[0]
widget = fitz.Widget()                 
widget.rect = fitz.Rect(10,10,50,50)           
widget.field_type = fitz.PDF_WIDGET_TYPE_BUTTON # also tried 1
widget.field_type_string = "Button"
widget.script = 'app.alert("clicked")'
widget.field_name = "btn"  
widget.field_value = "TEST"
annot = p.addWidget(widget)
doc.save("t2.pdf")

It works and executes the javascript when I clicked it. However, I cannot create a button with " fitz.PDF_WIDGET_TYPE_BUTTON ". It gave we a checkbox instead. I also tried to read the existed button to check the filed_type and field_type_string. I set the same value to that. But it still gave me the checkbox. How can I fix this? With the "widget.script" attribute I was able to write javascripts to the widget when clicked. Thank you for your help!

JorjMcKie commented 4 years ago

Godd that the script works! Unfortunately, PDF specification is a little tricky and non-intuitive in this are. First of all you do not need to set widget.field_type_string - this is a comment only for better interpretation.

I was successful in coding:

widget.field_type = fitz.PDF_WIDGET_TYPE_BUTTON
widget.field_flags = fitz.PDF_BTN_FIELD_IS_PUSHBUTTON

The widget.field_flags can also be further modified, but don't forget that it is a field of bit flags, so single values must be modified using boolean operators (&, |, etc.).

JorjMcKie commented 4 years ago

It is probably best to read section 8.6.3 Field Types of the PDF manual on page 685. A good working idea could also be gotten from the documentation here.

kurokawaikki commented 4 years ago

Since we cannot make the text in free text annotation become bold, we tried to use javascript instead. Thank for JorjMcKie's great help. Now, it is possible to have a workaround. Here is my final code to make the text become bold in free text annotation.

doc = fitz.open("tetetetet.pdf")
p = doc[0]
widget = fitz.Widget()
widget.rect = fitz.Rect(10,10,50,50)
widget.field_type = fitz.PDF_WIDGET_TYPE_TEXT 
widget.script = "var annt = this.getAnnots(); \n annt.forEach(function (item, index) { \n try{ \n var span = item.richContents; \n span.forEach(function (it, dx) {it.fontWeight = 800;}) \n item.richContents = span; \n}\n catch(err){} \n }); \n app.alert('Done');"
widget.field_name = "btn"
widget.field_value = "Make Bold"
widget.rotate = p.rotation
annot = p.addWidget(widget)
doc.save("t2.pdf")

I have one last question. Can I rotate the widget? I cannot rotate it with widget.rotate = p.rotation. Thank you very much!!

JorjMcKie commented 4 years ago

You can rotate text in a freetext annotation. For all annotations you can also control how it should react if the page is rotated (annotation flags). But you cannot rotate a widget currently.

JorjMcKie commented 4 years ago

@kurokawaikki - I find your solution very interesting! Can I persuade you to write an article in the Wiki of this repo? Or a simple description of what you did in a text file? I can offer to add it to PyMuPDF's documentation mentioning your contribution ... 😎

kurokawaikki commented 4 years ago

Yes, I would like to do it. I can write an article in the Wiki. What kind of contents should I proved to you? What should I write?

JorjMcKie commented 4 years ago

I think I would add it to the recipes section of the documentation and the Wiki pages. Title "How to modify a Freetext annotation font".

kurokawaikki commented 4 years ago

[Workaround] - How to make the text bold in a Freetext Annotation.

Problem: Since v1.16.0 a 'Freetext' annotation font is restricted to the "normal" versions of Times Roman, Helvetica, Courier - no more bold, italic. It is impossible to use pyMuPDF to modify the properties.

Solution: According to the documentation provided by the Adobe, it is possible to use Javascripts to manipulate the properties of Freetext annotation. Here is the API reference provided by Adobe (https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/js_api_reference.pdf) or (https://www.adobe.com/devnet/acrobat/documentation.html) for newer documentation. The Freetext annotation can be obtained by this.getAnnots() (this will return all annotation in the PDF as an array). Next, we can loop over this array to set the properties of the text through richContents attribute. There is no explicit property to set a bold text though, it is possible to set it through fontWeight = 800 (400 is the normal size) attribute of the richContents. Additionally, it is possible to set the color, italic and other font properties through richContents. Therefore, we can use pyMuPDF to create Freetext annotations in the document on the desired place first. Subsequently, use the new script attribute provided in the new pyMuPDF (version ID) to create an push button that links to the Javascript containing the code to make the text bold onClicked. Finally, we can export the PDF and open in the adobe reader (if you clicked it with PDF X-Change editor, all the annotation will disappear... Probably, it is a bug in PDF X-Change editor.). After clicking on the button, we can find that all the text in the Freetext annotation is Bold right now! You can remove the button by pyMuPDF or adobe acrobat (non-free) or PDF X-Change editor (free).

Here is a simple example to make a push button that can make the text in Freetext annotation bold.

import fitz

doc = fitz.open("test.pdf") #Open document
p = doc[0] #get the page object on page 1
widget = fitz.Widget() #create a widget
widget.rect = fitz.Rect(10,10,50,50) #set the size and position (x1, y1, x2, y2)
widget.field_type = fitz.PDF_WIDGET_TYPE_BUTTON
widget.field_flags = fitz.PDF_BTN_FIELD_IS_PUSHBUTTON #set the widget become push button
widget.script = "var annt = this.getAnnots(); \n annt.forEach(function (item, index) { \n try{ \n var span = item.richContents; \n span.forEach(function (it, dx) {it.fontWeight = 800;}) \n item.richContents = span; \n}\n catch(err){} \n }); \n app.alert('Done');" #set the specific Javascript that can make all annotation become bold.
widget.field_name = "btn"
widget.field_value = "Make Bold"
annot = p.addWidget(widget) # add the widget to the page
doc.save("t2.pdf") #output the file

With this workaround, you can set text properties that are not supported in the pyMuPDF. With the help from Javascript in PDF, it is possible to do much more stuffs.


If you need any other information, please let me know. Thank you very much for your great help.

JorjMcKie commented 4 years ago

@kurokawaikki

Hey thank you very much for this contribution indeed. I will publish it as promised in the documentation of the next PyMuPDF version, and also on Wiki. How can I reference you: just the Github name or do you wish to name your e-mail address or something else? Thanks again!

kurokawaikki commented 4 years ago

Thank you! It is okay just putting the Github name. I am very appreciate your work on pyMuPDF!

JorjMcKie commented 4 years ago

Official version 1.16.12 published.