PyPDFForm 4.2.0__tar.gz → 4.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {pypdfform-4.2.0 → pypdfform-4.2.2}/PKG-INFO +1 -1
  2. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/ap.py +31 -25
  4. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/template.py +2 -93
  5. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/watermark.py +2 -2
  6. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/wrapper.py +27 -38
  7. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm.egg-info/PKG-INFO +1 -1
  8. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_js.py +27 -0
  9. {pypdfform-4.2.0 → pypdfform-4.2.2}/LICENSE +0 -0
  10. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/adapter.py +0 -0
  11. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/assets/__init__.py +0 -0
  12. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/assets/bedrock.py +0 -0
  13. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/assets/blank.py +0 -0
  14. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/constants.py +0 -0
  15. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/coordinate.py +0 -0
  16. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/deprecation.py +0 -0
  17. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/filler.py +0 -0
  18. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/font.py +0 -0
  19. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/hooks.py +0 -0
  20. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/image.py +0 -0
  21. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/middleware/__init__.py +0 -0
  22. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/middleware/base.py +0 -0
  23. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/middleware/checkbox.py +0 -0
  24. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/middleware/dropdown.py +0 -0
  25. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/middleware/image.py +0 -0
  26. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/middleware/radio.py +0 -0
  27. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/middleware/signature.py +0 -0
  28. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/middleware/text.py +0 -0
  29. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/patterns.py +0 -0
  30. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/raw/__init__.py +0 -0
  31. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/raw/circle.py +0 -0
  32. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/raw/ellipse.py +0 -0
  33. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/raw/image.py +0 -0
  34. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/raw/line.py +0 -0
  35. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/raw/rect.py +0 -0
  36. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/raw/text.py +0 -0
  37. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/types.py +0 -0
  38. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/utils.py +0 -0
  39. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/widgets/__init__.py +0 -0
  40. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/widgets/base.py +0 -0
  41. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/widgets/checkbox.py +0 -0
  42. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/widgets/dropdown.py +0 -0
  43. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/widgets/image.py +0 -0
  44. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/widgets/radio.py +0 -0
  45. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/widgets/signature.py +0 -0
  46. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm/widgets/text.py +0 -0
  47. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  48. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  49. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm.egg-info/requires.txt +0 -0
  50. {pypdfform-4.2.0 → pypdfform-4.2.2}/PyPDFForm.egg-info/top_level.txt +0 -0
  51. {pypdfform-4.2.0 → pypdfform-4.2.2}/README.md +0 -0
  52. {pypdfform-4.2.0 → pypdfform-4.2.2}/pyproject.toml +0 -0
  53. {pypdfform-4.2.0 → pypdfform-4.2.2}/setup.cfg +0 -0
  54. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_bulk_create_fields.py +0 -0
  55. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_create_widget.py +0 -0
  56. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_draw_elements.py +0 -0
  57. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_dropdown.py +0 -0
  58. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_extract_values.py +0 -0
  59. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_fill_max_length_text_field.py +0 -0
  60. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_fill_method.py +0 -0
  61. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_font_widths.py +0 -0
  62. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_functional.py +0 -0
  63. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_generate_appearance_streams.py +0 -0
  64. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_need_appearances.py +0 -0
  65. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_paragraph.py +0 -0
  66. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_signature.py +0 -0
  67. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_use_full_widget_name.py +0 -0
  68. {pypdfform-4.2.0 → pypdfform-4.2.2}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.2.0
3
+ Version: 4.2.2
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -20,7 +20,7 @@ The library supports various PDF form features, including:
20
20
  PyPDFForm aims to simplify PDF form manipulation, making it accessible to developers of all skill levels.
21
21
  """
22
22
 
23
- __version__ = "4.2.0"
23
+ __version__ = "4.2.2"
24
24
 
25
25
  from .assets.blank import BlankPage
26
26
  from .middleware import Widgets
@@ -12,10 +12,10 @@ from io import BytesIO
12
12
 
13
13
  from pikepdf import Pdf
14
14
  from pypdf import PdfReader, PdfWriter
15
+ from pypdf.generic import DictionaryObject, NameObject, TextStringObject
15
16
 
16
- from .constants import XFA, AcroForm, Root
17
- from .template import (get_on_open_javascript, get_pdf_title,
18
- set_on_open_javascript, set_pdf_title)
17
+ from .constants import (JS, XFA, AcroForm, JavaScript, OpenAction, Root, S,
18
+ Title)
19
19
  from .utils import stream_to_io
20
20
 
21
21
 
@@ -31,8 +31,6 @@ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) ->
31
31
  PDF viewers to generate appearance streams for form fields.
32
32
  3. Optionally generating appearance streams explicitly using pikepdf if
33
33
  `generate_appearance_streams` is True.
34
- 4. Preserving the title from the original PDF.
35
- 5. Preserving the on-open JavaScript from the original PDF.
36
34
 
37
35
  The result is cached using lru_cache for performance.
38
36
 
@@ -65,35 +63,43 @@ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) ->
65
63
  r.seek(0)
66
64
  result = r.read()
67
65
 
68
- result = preserve_title(pdf, result)
69
- return preserve_on_open_javascript(pdf, result)
66
+ return result
70
67
 
71
68
 
72
- def preserve_title(src: bytes, dest: bytes) -> bytes:
69
+ @lru_cache
70
+ def preserve_pdf_properties(pdf: bytes, title: str, script: str) -> bytes:
73
71
  """
74
- Preserves the title from the source PDF to the destination PDF.
72
+ Preserves and updates PDF properties such as title and OpenAction scripts.
73
+
74
+ This function allows setting or updating the PDF's title in its metadata and
75
+ attaching a JavaScript script that executes when the PDF is opened.
75
76
 
76
77
  Args:
77
- src (bytes): The source PDF file content as a bytes stream.
78
- dest (bytes): The destination PDF file content as a bytes stream.
78
+ pdf (bytes): The PDF file content as a bytes stream.
79
+ title (str): The title to be set in the PDF metadata.
80
+ script (str): JavaScript code to be executed when the PDF is opened.
79
81
 
80
82
  Returns:
81
- bytes: The modified destination PDF content as a bytes stream.
83
+ bytes: The modified PDF content as a bytes stream.
82
84
  """
83
- title = get_pdf_title(src)
84
- return set_pdf_title(dest, title)
85
+ reader = PdfReader(stream_to_io(pdf))
86
+ writer = PdfWriter()
87
+ writer.append(reader)
85
88
 
89
+ if title:
90
+ metadata = reader.metadata or {}
91
+ metadata[NameObject(Title)] = TextStringObject(title)
86
92
 
87
- def preserve_on_open_javascript(src: bytes, dest: bytes) -> bytes:
88
- """
89
- Preserves the on-open JavaScript from the source PDF to the destination PDF.
93
+ writer.add_metadata(metadata)
90
94
 
91
- Args:
92
- src (bytes): The source PDF file content as a bytes stream.
93
- dest (bytes): The destination PDF file content as a bytes stream.
95
+ if script:
96
+ open_action = DictionaryObject()
97
+ open_action[NameObject(S)] = NameObject(JavaScript)
98
+ open_action[NameObject(JS)] = TextStringObject(script)
94
99
 
95
- Returns:
96
- bytes: The modified destination PDF content as a bytes stream.
97
- """
98
- script = get_on_open_javascript(src)
99
- return set_on_open_javascript(dest, script)
100
+ writer._root_object.update({NameObject(OpenAction): open_action}) # type: ignore # noqa: SLF001 # # pylint: disable=W0212
101
+
102
+ with BytesIO() as f:
103
+ writer.write(f)
104
+ f.seek(0)
105
+ return f.read()
@@ -13,10 +13,9 @@ from io import BytesIO
13
13
  from typing import Dict, List, Tuple, Union, cast
14
14
 
15
15
  from pypdf import PdfReader, PdfWriter
16
- from pypdf.generic import DictionaryObject, NameObject, TextStringObject
16
+ from pypdf.generic import DictionaryObject
17
17
 
18
- from .constants import (JS, WIDGET_TYPES, Annots, JavaScript, MaxLen,
19
- OpenAction, Parent, S, T, Title)
18
+ from .constants import WIDGET_TYPES, Annots, MaxLen, Parent, T
20
19
  from .middleware.checkbox import Checkbox
21
20
  from .middleware.dropdown import Dropdown
22
21
  from .middleware.radio import Radio
@@ -244,96 +243,6 @@ def get_dropdown_choices(widget: dict) -> Union[Tuple[str, ...], None]:
244
243
  )
245
244
 
246
245
 
247
- def get_on_open_javascript(pdf: bytes) -> Union[str, None]:
248
- """
249
- Retrieves the JavaScript that runs when the PDF is opened.
250
-
251
- Args:
252
- pdf (bytes): The PDF file content as a bytes stream.
253
-
254
- Returns:
255
- Union[str, None]: The JavaScript that runs when the PDF is opened, or None if it's not present.
256
- """
257
- reader = PdfReader(stream_to_io(pdf))
258
- try:
259
- return reader.root_object[OpenAction][JS]
260
- except KeyError:
261
- return None
262
-
263
-
264
- def set_on_open_javascript(pdf: bytes, script: str) -> bytes:
265
- """
266
- Sets the JavaScript that runs when the PDF is opened.
267
-
268
- Args:
269
- pdf (bytes): The PDF file content as a bytes stream.
270
- script (str): The JavaScript to run when the PDF is opened.
271
-
272
- Returns:
273
- bytes: The modified PDF content as a bytes stream.
274
- """
275
- if not script:
276
- return pdf
277
-
278
- reader = PdfReader(stream_to_io(pdf))
279
- writer = PdfWriter()
280
- writer.append(reader)
281
-
282
- open_action = DictionaryObject()
283
- open_action[NameObject(S)] = NameObject(JavaScript)
284
- open_action[NameObject(JS)] = TextStringObject(script)
285
-
286
- writer._root_object.update({NameObject(OpenAction): open_action}) # type: ignore # noqa: SLF001 # # pylint: disable=W0212
287
-
288
- with BytesIO() as f:
289
- writer.write(f)
290
- f.seek(0)
291
- return f.read()
292
-
293
-
294
- def get_pdf_title(pdf: bytes) -> Union[str, None]:
295
- """
296
- Retrieves the title of a PDF from its metadata.
297
-
298
- Args:
299
- pdf (bytes): The PDF file content as a bytes stream.
300
-
301
- Returns:
302
- Union[str, None]: The title of the PDF, or None if it's not present.
303
- """
304
- reader = PdfReader(stream_to_io(pdf))
305
- return (reader.metadata or {}).get(Title)
306
-
307
-
308
- def set_pdf_title(pdf: bytes, title: str) -> bytes:
309
- """
310
- Sets the title of a PDF in its metadata.
311
-
312
- Args:
313
- pdf (bytes): The PDF file content as a bytes stream.
314
- title (str): The new title for the PDF.
315
-
316
- Returns:
317
- bytes: The modified PDF content as a bytes stream.
318
- """
319
- if not title:
320
- return pdf
321
-
322
- reader = PdfReader(stream_to_io(pdf))
323
- writer = PdfWriter()
324
- writer.append(reader)
325
-
326
- metadata = reader.metadata or {}
327
- metadata[NameObject(Title)] = TextStringObject(title)
328
-
329
- writer.add_metadata(metadata)
330
-
331
- with BytesIO() as f:
332
- writer.write(f)
333
- f.seek(0)
334
- return f.read()
335
-
336
-
337
246
  def update_widget_keys(
338
247
  template: bytes,
339
248
  widgets: Dict[str, WIDGET_TYPES],
@@ -305,13 +305,13 @@ def merge_watermarks_with_pdf(
305
305
  result = BytesIO()
306
306
  pdf_file = PdfReader(stream_to_io(pdf))
307
307
  output = PdfWriter()
308
+ output.append(pdf_file)
308
309
 
309
- for i, page in enumerate(pdf_file.pages):
310
+ for i, page in enumerate(output.pages):
310
311
  if watermarks[i]:
311
312
  watermark = PdfReader(stream_to_io(watermarks[i]))
312
313
  if watermark.pages:
313
314
  page.merge_page(watermark.pages[0])
314
- output.add_page(page)
315
315
 
316
316
  output.write(result)
317
317
  result.seek(0)
@@ -27,7 +27,7 @@ from typing import (TYPE_CHECKING, BinaryIO, Dict, Sequence, TextIO, Tuple,
27
27
 
28
28
  from .adapter import (fp_or_f_obj_or_f_content_to_content,
29
29
  fp_or_f_obj_or_stream_to_stream)
30
- from .ap import appearance_streams_handler
30
+ from .ap import appearance_streams_handler, preserve_pdf_properties
31
31
  from .constants import VERSION_IDENTIFIER_PREFIX, VERSION_IDENTIFIERS
32
32
  from .coordinate import generate_coordinate_grid
33
33
  from .filler import fill
@@ -38,9 +38,7 @@ from .middleware.dropdown import Dropdown
38
38
  from .middleware.signature import Signature
39
39
  from .middleware.text import Text
40
40
  from .raw import RawText, RawTypes
41
- from .template import (build_widgets, get_on_open_javascript, get_pdf_title,
42
- set_on_open_javascript, set_pdf_title,
43
- update_widget_keys)
41
+ from .template import build_widgets, update_widget_keys
44
42
  from .types import PdfWrapperList
45
43
  from .utils import (generate_unique_suffix, get_page_streams, merge_pdfs,
46
44
  remove_all_widgets)
@@ -106,6 +104,8 @@ class PdfWrapper:
106
104
  super().__init__()
107
105
  self._stream = fp_or_f_obj_or_stream_to_stream(template)
108
106
  self.widgets = {}
107
+ self.title: str = None
108
+ self._on_open_javascript = None
109
109
  self._available_fonts = {} # for setting /F1
110
110
  self._font_register_events = [] # for reregister
111
111
  self._key_update_tracker = {} # for update key preserve old key attrs
@@ -223,28 +223,6 @@ class PdfWrapper:
223
223
 
224
224
  return self
225
225
 
226
- @property
227
- def title(self) -> Union[str, None]:
228
- """
229
- Returns the title of the PDF document.
230
-
231
- Returns:
232
- Union[str, None]: The title of the PDF, or None if it's not set.
233
- """
234
-
235
- return get_pdf_title(self._read())
236
-
237
- @title.setter
238
- def title(self, value: str) -> None:
239
- """
240
- Sets the title of the PDF document.
241
-
242
- Args:
243
- value (str): The new title for the PDF document.
244
- """
245
-
246
- self._stream = set_pdf_title(self._read(), value)
247
-
248
226
  @property
249
227
  def schema(self) -> dict:
250
228
  """
@@ -348,37 +326,41 @@ class PdfWrapper:
348
326
  @property
349
327
  def on_open_javascript(self) -> Union[str, None]:
350
328
  """
351
- Returns the JavaScript that runs when the PDF is opened.
329
+ Returns the JavaScript script that executes when the PDF is opened.
352
330
 
353
331
  Returns:
354
- Union[str, None]: The JavaScript that runs when the PDF is opened, or None if it's not set.
332
+ Union[str, None]: The JavaScript script, or None if no script is set.
355
333
  """
356
334
 
357
- return get_on_open_javascript(self._read())
335
+ return self._on_open_javascript
358
336
 
359
337
  @on_open_javascript.setter
360
338
  def on_open_javascript(self, value: Union[str, TextIO]) -> None:
361
339
  """
362
- Sets the JavaScript that runs when the PDF is opened.
340
+ Sets the JavaScript script that executes when the PDF is opened.
363
341
 
364
342
  Args:
365
- value (Union[str, TextIO]): The JavaScript to run when the PDF is opened.
366
- Can be a string or a text file-like object.
343
+ value (Union[str, TextIO]): The JavaScript script, provided as either:
344
+ - str: The JavaScript code as a string, or a file path to a .js file.
345
+ - TextIO: An open file-like object containing the JavaScript code.
367
346
  """
368
347
 
369
- self._stream = set_on_open_javascript(
370
- self._read(), fp_or_f_obj_or_f_content_to_content(value)
371
- )
348
+ self._on_open_javascript = fp_or_f_obj_or_f_content_to_content(value)
372
349
 
373
350
  def read(self) -> bytes:
374
351
  """
375
352
  Reads the PDF document and returns its content as bytes.
376
353
 
377
- This method retrieves the PDF stream and optionally generates appearance
378
- streams for form fields if `need_appearances` is enabled.
354
+ This method retrieves the PDF stream and performs several processing steps:
355
+ 1. Triggers widget hooks and updates font mappings (via `_read`).
356
+ 2. If `need_appearances` is enabled, it handles appearance streams and the
357
+ `/NeedAppearances` flag, which may include removing XFA and explicitly
358
+ generating appearance streams.
359
+ 3. If a title or on-open JavaScript is set, it updates the PDF properties
360
+ accordingly.
379
361
 
380
362
  Returns:
381
- bytes: The PDF document content as a byte string.
363
+ bytes: The processed PDF document content as a byte string.
382
364
  """
383
365
 
384
366
  result = self._read()
@@ -387,6 +369,13 @@ class PdfWrapper:
387
369
  result, getattr(self, "generate_appearance_streams")
388
370
  ) # cached
389
371
 
372
+ if any([self.title, self.on_open_javascript]):
373
+ result = preserve_pdf_properties(
374
+ result,
375
+ self.title,
376
+ self.on_open_javascript,
377
+ )
378
+
390
379
  return result
391
380
 
392
381
  def _read(self) -> bytes:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.2.0
3
+ Version: 4.2.2
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -220,3 +220,30 @@ def test_on_open_script(template_stream, pdf_samples, request):
220
220
 
221
221
  assert len(pdf.read()) == len(expected)
222
222
  assert pdf.read() == expected
223
+
224
+
225
+ def test_on_open_script_title_coexist(template_stream, pdf_samples, request):
226
+ expected_path = os.path.join(
227
+ pdf_samples, "js", "test_on_open_script_title_coexist.pdf"
228
+ )
229
+ with open(expected_path, "rb+") as f:
230
+ pdf = PdfWrapper(template_stream, title="My PDF")
231
+ pdf.on_open_javascript = 'this.getField("test").value = "opened";'
232
+
233
+ assert pdf.title == "My PDF"
234
+ assert pdf.on_open_javascript == 'this.getField("test").value = "opened";'
235
+
236
+ request.config.results["expected_path"] = expected_path
237
+ request.config.results["stream"] = pdf.read()
238
+
239
+ expected = f.read()
240
+
241
+ assert len(pdf.read()) == len(expected)
242
+ assert pdf.read() == expected
243
+
244
+ pdf2 = PdfWrapper(template_stream)
245
+ pdf2.on_open_javascript = 'this.getField("test").value = "opened";'
246
+ pdf2.title = "My PDF"
247
+
248
+ assert len(pdf2.read()) == len(expected)
249
+ assert pdf2.read() == expected
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes