PyPDFForm 5.4.0__tar.gz → 5.5.0__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 (86) hide show
  1. {pypdfform-5.4.0 → pypdfform-5.5.0}/PKG-INFO +1 -1
  2. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/inspect.py +26 -4
  4. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/root.py +3 -1
  5. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/constants.py +0 -1
  6. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/egress.py +9 -64
  7. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/filler.py +2 -4
  8. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/font.py +1 -3
  9. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/hooks.py +2 -4
  10. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/template.py +99 -9
  11. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/utils.py +2 -4
  12. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/watermark.py +2 -5
  13. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/wrapper.py +63 -35
  14. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm.egg-info/PKG-INFO +1 -1
  15. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_functional.py +8 -0
  16. {pypdfform-5.4.0 → pypdfform-5.5.0}/LICENSE +0 -0
  17. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/__init__.py +0 -0
  18. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/common.py +0 -0
  19. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/create.py +0 -0
  20. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/entry.py +0 -0
  21. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/remove.py +0 -0
  22. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/schemas/__init__.py +0 -0
  23. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/schemas/create.py +0 -0
  24. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/schemas/update.py +0 -0
  25. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/cli/update.py +0 -0
  26. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/__init__.py +0 -0
  27. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/adapter.py +0 -0
  28. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/__init__.py +0 -0
  29. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/base.py +0 -0
  30. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/link.py +0 -0
  31. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/stamp.py +0 -0
  32. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/text.py +0 -0
  33. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  34. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/assets/__init__.py +0 -0
  35. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/assets/bedrock.py +0 -0
  36. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/assets/blank.py +0 -0
  37. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/coordinate.py +0 -0
  38. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/deprecation.py +0 -0
  39. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/image.py +0 -0
  40. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/__init__.py +0 -0
  41. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/base.py +0 -0
  42. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  43. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  44. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/image.py +0 -0
  45. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/radio.py +0 -0
  46. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/signature.py +0 -0
  47. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/text.py +0 -0
  48. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/patterns.py +0 -0
  49. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/raw/__init__.py +0 -0
  50. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/raw/circle.py +0 -0
  51. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/raw/ellipse.py +0 -0
  52. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/raw/image.py +0 -0
  53. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/raw/line.py +0 -0
  54. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/raw/rect.py +0 -0
  55. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/raw/text.py +0 -0
  56. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/types.py +0 -0
  57. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/__init__.py +0 -0
  58. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/base.py +0 -0
  59. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  60. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  61. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/image.py +0 -0
  62. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/radio.py +0 -0
  63. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/signature.py +0 -0
  64. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/text.py +0 -0
  65. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  66. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  67. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm.egg-info/entry_points.txt +0 -0
  68. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm.egg-info/requires.txt +0 -0
  69. {pypdfform-5.4.0 → pypdfform-5.5.0}/PyPDFForm.egg-info/top_level.txt +0 -0
  70. {pypdfform-5.4.0 → pypdfform-5.5.0}/README.md +0 -0
  71. {pypdfform-5.4.0 → pypdfform-5.5.0}/pyproject.toml +0 -0
  72. {pypdfform-5.4.0 → pypdfform-5.5.0}/setup.cfg +0 -0
  73. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_bulk_create_fields.py +0 -0
  74. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_create_widget.py +0 -0
  75. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_draw_elements.py +0 -0
  76. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_dropdown.py +0 -0
  77. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_extract_middleware_attributes.py +0 -0
  78. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_fill_max_length_text_field.py +0 -0
  79. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_font_widths.py +0 -0
  80. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_generate_appearance_streams.py +0 -0
  81. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_js.py +0 -0
  82. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_need_appearances.py +0 -0
  83. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_paragraph.py +0 -0
  84. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_signature.py +0 -0
  85. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_use_full_widget_name.py +0 -0
  86. {pypdfform-5.4.0 → pypdfform-5.5.0}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 5.4.0
3
+ Version: 5.5.0
4
4
  Summary: The Python library & CLI for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -18,7 +18,7 @@ from Python code or from the command line.
18
18
 
19
19
  import logging
20
20
 
21
- __version__ = "5.4.0"
21
+ __version__ = "5.5.0"
22
22
 
23
23
  from .lib.annotations import Annotations
24
24
  from .lib.assets.blank import BlankPage
@@ -2,10 +2,10 @@
2
2
  """
3
3
  This module defines CLI commands for inspecting PDF form information.
4
4
 
5
- It exposes the `inspect` command group, which prints JSON for form schemas,
6
- current form values, generated sample data, and field rectangle metadata.
7
- Each command wraps read-only `PdfWrapper` properties so users can inspect forms
8
- from the terminal without writing Python code.
5
+ It exposes the `inspect` command group, which prints PDF titles and JSON for
6
+ form schemas, current form values, generated sample data, and field rectangle
7
+ metadata. Each command wraps read-only `PdfWrapper` properties so users can
8
+ inspect forms from the terminal without writing Python code.
9
9
  """
10
10
 
11
11
  import json
@@ -20,6 +20,28 @@ inspect_cli = typer.Typer(
20
20
  )
21
21
 
22
22
 
23
+ @inspect_cli.command(
24
+ no_args_is_help=True,
25
+ help="Print the PDF title.",
26
+ )
27
+ def title(
28
+ ctx: typer.Context,
29
+ pdf: INPUT_PDF,
30
+ ) -> None:
31
+ """
32
+ Print the document title metadata from an existing PDF.
33
+
34
+ The command loads the PDF with the global CLI options stored in `ctx.obj`,
35
+ reads `PdfWrapper.title`, and writes it to standard output.
36
+
37
+ Args:
38
+ ctx (typer.Context): Typer context containing global `PdfWrapper`
39
+ options in `ctx.obj`.
40
+ pdf (Path): Input PDF path.
41
+ """
42
+ typer.echo(PdfWrapper(str(pdf), **ctx.obj).title)
43
+
44
+
23
45
  @inspect_cli.command(
24
46
  no_args_is_help=True,
25
47
  help="Print the form schema as JSON.",
@@ -113,6 +113,7 @@ def main(
113
113
  typer.Option(
114
114
  "--preserve-metadata",
115
115
  help="Preserve input PDF metadata.",
116
+ hidden=True,
116
117
  ),
117
118
  ] = False,
118
119
  use_full_widget_name: Annotated[
@@ -137,7 +138,8 @@ def main(
137
138
  field appearances.
138
139
  generate_appearance_streams (bool): Whether to generate form field
139
140
  appearance streams while handling PDFs.
140
- preserve_metadata (bool): Whether to preserve input PDF metadata.
141
+ preserve_metadata (bool): Deprecated compatibility option. Input PDF
142
+ metadata is preserved automatically.
141
143
  use_full_widget_name (bool): Whether widget lookups should use full
142
144
  form field names.
143
145
  """
@@ -122,7 +122,6 @@ FONT_NAME_PREFIX = "/F"
122
122
 
123
123
  # For Adobe Acrobat
124
124
  AcroForm = "/AcroForm"
125
- Root = "/Root"
126
125
  Fields = "/Fields"
127
126
  XFA = "/XFA"
128
127
 
@@ -5,11 +5,9 @@ A module for egress functions.
5
5
  This module provides functionalities that prepare the final PDF for output (egress),
6
6
  ensuring that it is properly formatted and ready for the end-user. This includes
7
7
  managing appearance streams (so form fields display correctly after being filled),
8
- handling the /NeedAppearances flag, and preserving or updating document-level
9
- properties like metadata, title, and OpenAction scripts. It can also rebuild the
10
- AcroForm `/Fields` array from the widget annotations present on each page. These
11
- functions are typically called right before the final PDF byte stream is returned by
12
- the wrapper module.
8
+ handling the /NeedAppearances flag, and rebuilding the AcroForm `/Fields` array from
9
+ the widget annotations present on each page. These functions are typically called
10
+ right before the final PDF byte stream is returned by the wrapper module.
13
11
  """
14
12
 
15
13
  from functools import lru_cache
@@ -17,21 +15,15 @@ from io import BytesIO
17
15
  from warnings import catch_warnings, filterwarnings
18
16
 
19
17
  from pikepdf import Pdf
20
- from pypdf import PdfReader, PdfWriter
21
- from pypdf.generic import ArrayObject, DictionaryObject, NameObject, TextStringObject
18
+ from pypdf import PdfWriter
19
+ from pypdf.generic import ArrayObject, DictionaryObject, NameObject
22
20
 
23
21
  from .constants import (
24
- JS,
25
22
  XFA,
26
23
  AcroForm,
27
24
  Annots,
28
25
  Fields,
29
- JavaScript,
30
- OpenAction,
31
26
  Parent,
32
- Root,
33
- S,
34
- Title,
35
27
  )
36
28
  from .template import get_widget_key
37
29
 
@@ -58,13 +50,12 @@ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) ->
58
50
  Returns:
59
51
  bytes: The modified PDF content as a bytes stream.
60
52
  """
61
- reader = PdfReader(BytesIO(pdf))
62
- writer = PdfWriter()
53
+ writer = PdfWriter(BytesIO(pdf))
63
54
 
64
- if AcroForm in reader.trailer[Root] and XFA in reader.trailer[Root][AcroForm]:
65
- del reader.trailer[Root][AcroForm][XFA]
55
+ root_object = writer._root_object # type: ignore # noqa: SLF001 # # pylint: disable=W0212
56
+ if AcroForm in root_object and XFA in root_object[AcroForm]:
57
+ del root_object[AcroForm][XFA]
66
58
 
67
- writer.append(reader)
68
59
  writer.set_need_appearances_writer()
69
60
 
70
61
  with BytesIO() as f:
@@ -87,52 +78,6 @@ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) ->
87
78
  return result
88
79
 
89
80
 
90
- def preserve_pdf_properties(
91
- pdf: bytes, title: str, script: str, metadata: dict
92
- ) -> bytes:
93
- """
94
- Preserves and updates PDF properties such as metadata, title, and OpenAction scripts.
95
-
96
- This function allows setting or updating the PDF's title and metadata, and
97
- attaching a JavaScript script that executes when the PDF is opened. Metadata
98
- is merged into the reader's current metadata when provided; the title and
99
- OpenAction JavaScript are written only when non-empty values are supplied.
100
-
101
- Args:
102
- pdf (bytes): The PDF file content as a bytes stream.
103
- title (str): The title to be set in the PDF metadata.
104
- script (str): JavaScript code to be executed when the PDF is opened.
105
- metadata (dict): The original metadata to preserve.
106
-
107
- Returns:
108
- bytes: The modified PDF content as a bytes stream.
109
- """
110
- reader = PdfReader(BytesIO(pdf))
111
- writer = PdfWriter()
112
- writer.append(reader)
113
-
114
- if title or metadata:
115
- _metadata = reader.metadata or {}
116
- if metadata:
117
- _metadata.update(metadata)
118
- if title:
119
- _metadata[NameObject(Title)] = TextStringObject(title)
120
-
121
- writer.add_metadata(_metadata)
122
-
123
- if script:
124
- open_action = DictionaryObject()
125
- open_action[NameObject(S)] = NameObject(JavaScript)
126
- open_action[NameObject(JS)] = TextStringObject(script)
127
-
128
- writer._root_object.update({NameObject(OpenAction): open_action}) # type: ignore # noqa: SLF001 # # pylint: disable=W0212
129
-
130
- with BytesIO() as f:
131
- writer.write(f)
132
- f.seek(0)
133
- return f.read()
134
-
135
-
136
81
  def rebuild_acroform_fields(
137
82
  pdf: bytes, widget_keys: set, use_full_widget_name: bool
138
83
  ) -> bytes:
@@ -11,7 +11,7 @@ supports flattening the filled form to prevent further modifications.
11
11
  from io import BytesIO
12
12
  from typing import Dict, cast
13
13
 
14
- from pypdf import PdfReader, PdfWriter
14
+ from pypdf import PdfWriter
15
15
  from pypdf.generic import DictionaryObject
16
16
 
17
17
  from .constants import Annots
@@ -188,9 +188,7 @@ def fill(
188
188
  stream as bytes when an image or signature was drawn. The second
189
189
  tuple item is None when no image drawing is needed.
190
190
  """
191
- pdf = PdfReader(BytesIO(template))
192
- out = PdfWriter()
193
- out.append(pdf)
191
+ out = PdfWriter(BytesIO(template))
194
192
 
195
193
  radio_button_tracker = {}
196
194
  images_to_draw = {}
@@ -276,9 +276,7 @@ def register_font_acroform(
276
276
  (str) that was assigned to the registered font within the PDF.
277
277
  """
278
278
  base_font_name = _get_base_font_name(ttf_stream)
279
- reader = PdfReader(BytesIO(pdf))
280
- writer = PdfWriter()
281
- writer.append(reader)
279
+ writer = PdfWriter(BytesIO(pdf))
282
280
 
283
281
  font_descriptor_params = {}
284
282
  font_dict_params = {}
@@ -13,7 +13,7 @@ import sys
13
13
  from io import BytesIO
14
14
  from typing import TextIO, cast
15
15
 
16
- from pypdf import PdfReader, PdfWriter
16
+ from pypdf import PdfWriter
17
17
  from pypdf.generic import (
18
18
  ArrayObject,
19
19
  DictionaryObject,
@@ -84,9 +84,7 @@ def trigger_widget_hooks(
84
84
  Returns:
85
85
  bytes: The modified PDF data as bytes, with the widget hooks applied.
86
86
  """
87
- pdf_file = PdfReader(BytesIO(pdf))
88
- output = PdfWriter()
89
- output.append(pdf_file)
87
+ output = PdfWriter(BytesIO(pdf))
90
88
 
91
89
  for page in output.pages:
92
90
  for annot in page.get(Annots, []):
@@ -2,10 +2,10 @@
2
2
  """
3
3
  Module for handling PDF form templates.
4
4
 
5
- This module provides functionalities to extract, build, and update widgets
6
- in PDF form templates. It leverages the pypdf library for PDF manipulation
7
- and defines specific patterns for identifying and constructing different
8
- types of widgets.
5
+ This module provides functionalities to read and update document metadata, set
6
+ document-level actions, and extract, build, and update widgets in PDF form
7
+ templates. It leverages the pypdf library for PDF manipulation and defines
8
+ specific patterns for identifying and constructing different types of widgets.
9
9
  """
10
10
 
11
11
  from copy import deepcopy
@@ -14,10 +14,21 @@ from io import BytesIO
14
14
  from typing import Dict, List, cast
15
15
 
16
16
  from pypdf import PdfReader, PdfWriter
17
- from pypdf.generic import ArrayObject, DictionaryObject, NameObject
17
+ from pypdf.generic import ArrayObject, DictionaryObject, NameObject, TextStringObject
18
18
 
19
19
  from .annotations import AnnotationTypes
20
- from .constants import COMB, MULTILINE, READ_ONLY, REQUIRED, Annots
20
+ from .constants import (
21
+ COMB,
22
+ JS,
23
+ MULTILINE,
24
+ READ_ONLY,
25
+ REQUIRED,
26
+ Annots,
27
+ JavaScript,
28
+ OpenAction,
29
+ S,
30
+ Title,
31
+ )
21
32
  from .middleware import WIDGET_TYPES
22
33
  from .middleware.checkbox import Checkbox
23
34
  from .middleware.dropdown import Dropdown
@@ -60,6 +71,87 @@ def get_metadata(pdf: bytes) -> dict:
60
71
  return reader.metadata or {}
61
72
 
62
73
 
74
+ def get_title(pdf: bytes) -> str | None:
75
+ """
76
+ Retrieves the title from a PDF's document metadata.
77
+
78
+ Args:
79
+ pdf (bytes): The PDF stream from which to retrieve the title.
80
+
81
+ Returns:
82
+ str | None: The document title, or None when no title is present.
83
+ """
84
+ return get_metadata(pdf).get(Title)
85
+
86
+
87
+ def set_metadata(pdf: bytes, metadata: dict) -> bytes:
88
+ """
89
+ Merges metadata into a PDF's existing document metadata.
90
+
91
+ Existing entries are retained unless the supplied mapping contains the
92
+ same key, in which case the supplied value replaces the existing value.
93
+
94
+ Args:
95
+ pdf (bytes): The PDF stream whose metadata should be updated.
96
+ metadata (dict): Metadata entries to add or replace.
97
+
98
+ Returns:
99
+ bytes: The PDF stream containing the merged metadata.
100
+ """
101
+ writer = PdfWriter(BytesIO(pdf))
102
+ _metadata = writer.metadata or {}
103
+ _metadata.update(metadata)
104
+ writer.add_metadata(_metadata)
105
+
106
+ with BytesIO() as f:
107
+ writer.write(f)
108
+ f.seek(0)
109
+ return f.read()
110
+
111
+
112
+ def set_title(pdf: bytes, title: str) -> bytes:
113
+ """
114
+ Sets a PDF's document title while preserving its other metadata.
115
+
116
+ Args:
117
+ pdf (bytes): The PDF stream whose title should be set.
118
+ title (str): The document title to store in the PDF metadata.
119
+
120
+ Returns:
121
+ bytes: The PDF stream containing the updated title.
122
+ """
123
+ return set_metadata(pdf, {Title: title})
124
+
125
+
126
+ def set_on_open_javascript(pdf: bytes, script: str) -> bytes:
127
+ """
128
+ Sets a PDF's document-open action to execute JavaScript.
129
+
130
+ The script is written to the document catalog as a JavaScript
131
+ `/OpenAction`, replacing any existing document-open action. An action is
132
+ written even when `script` is empty.
133
+
134
+ Args:
135
+ pdf (bytes): The PDF stream whose document-open action should be set.
136
+ script (str): JavaScript to execute when the PDF is opened.
137
+
138
+ Returns:
139
+ bytes: The PDF stream containing the JavaScript `/OpenAction`.
140
+ """
141
+ writer = PdfWriter(BytesIO(pdf))
142
+
143
+ open_action = DictionaryObject()
144
+ open_action[NameObject(S)] = NameObject(JavaScript)
145
+ open_action[NameObject(JS)] = TextStringObject(script)
146
+
147
+ writer._root_object.update({NameObject(OpenAction): open_action}) # type: ignore # noqa: SLF001 # # pylint: disable=W0212
148
+
149
+ with BytesIO() as f:
150
+ writer.write(f)
151
+ f.seek(0)
152
+ return f.read()
153
+
154
+
63
155
  def build_widgets(
64
156
  pdf_stream: bytes,
65
157
  use_full_widget_name: bool,
@@ -496,9 +588,7 @@ def update_widget_keys(
496
588
  Returns:
497
589
  bytes: The updated PDF template as a byte stream.
498
590
  """
499
- pdf = PdfReader(BytesIO(template))
500
- out = PdfWriter()
501
- out.append(pdf)
591
+ out = PdfWriter(BytesIO(template))
502
592
 
503
593
  _apply_widget_key_updates(out, widgets, old_keys, new_keys, indices)
504
594
 
@@ -46,13 +46,11 @@ def remove_all_widgets(pdf: bytes) -> bytes:
46
46
  Returns:
47
47
  bytes: The PDF with all widgets removed, as a bytes stream.
48
48
  """
49
- pdf_file = PdfReader(BytesIO(pdf))
50
49
  result_stream = BytesIO()
51
- writer = PdfWriter()
52
- for page in pdf_file.pages:
50
+ writer = PdfWriter(BytesIO(pdf))
51
+ for page in writer.pages:
53
52
  if page.annotations:
54
53
  page.annotations.clear()
55
- writer.add_page(page)
56
54
 
57
55
  writer.write(result_stream)
58
56
  result_stream.seek(0)
@@ -326,9 +326,7 @@ def merge_watermarks_with_pdf(
326
326
  bytes: A byte stream representing the merged PDF with watermarks applied.
327
327
  """
328
328
  result = BytesIO()
329
- pdf_file = PdfReader(BytesIO(pdf))
330
- output = PdfWriter()
331
- output.append(pdf_file)
329
+ output = PdfWriter(BytesIO(pdf))
332
330
 
333
331
  for i, page in enumerate(output.pages):
334
332
  if watermarks[i]:
@@ -546,8 +544,7 @@ def copy_watermark_widgets(
546
544
  Returns:
547
545
  bytes: The modified PDF byte stream with copied widgets.
548
546
  """
549
- pdf_writer = PdfWriter()
550
- pdf_writer.append(PdfReader(BytesIO(pdf)))
547
+ pdf_writer = PdfWriter(BytesIO(pdf))
551
548
 
552
549
  widgets_to_copy = _collect_widgets_to_copy(pdf_writer, watermarks, keys, page_num)
553
550
  _apply_widgets_to_pages(pdf_writer, widgets_to_copy)
@@ -27,7 +27,6 @@ from typing import (
27
27
  BinaryIO,
28
28
  Dict,
29
29
  List,
30
- Optional,
31
30
  Sequence,
32
31
  TextIO,
33
32
  Tuple,
@@ -38,9 +37,9 @@ from .adapter import (
38
37
  fp_or_f_obj_or_stream_to_stream,
39
38
  )
40
39
  from .coordinate import generate_coordinate_grid
40
+ from .deprecation import deprecation_notice
41
41
  from .egress import (
42
42
  appearance_streams_handler,
43
- preserve_pdf_properties,
44
43
  rebuild_acroform_fields,
45
44
  )
46
45
  from .filler import fill
@@ -58,7 +57,10 @@ from .template import (
58
57
  build_widgets,
59
58
  create_annotations,
60
59
  get_metadata,
60
+ get_title,
61
61
  remove_widgets_by_keys,
62
+ set_on_open_javascript,
63
+ set_title,
62
64
  update_widget_keys,
63
65
  )
64
66
  from .types import PdfArray
@@ -107,8 +109,11 @@ class PdfWrapper:
107
109
  - `use_full_widget_name` (bool): Whether to use the full widget name when filling the form.
108
110
  - `need_appearances` (bool): Whether to set the `NeedAppearances` flag in the PDF's AcroForm dictionary.
109
111
  - `generate_appearance_streams` (bool): Whether to explicitly generate appearance streams for all form fields using pikepdf.
110
- - `preserve_metadata` (bool): Whether to preserve the original metadata of the PDF.
111
- - `title` (str): The title of the PDF document.
112
+ - `preserve_metadata` (bool): Deprecated compatibility attribute;
113
+ input PDF metadata is captured automatically.
114
+ - `title` (str | None): The title stored in the PDF's document
115
+ metadata. A non-None value replaces the existing title; None
116
+ preserves it.
112
117
 
113
118
  """
114
119
 
@@ -129,9 +134,12 @@ class PdfWrapper:
129
134
  Constructor method for the `PdfWrapper` class.
130
135
 
131
136
  Initializes a new `PdfWrapper` object with the given template PDF and optional keyword arguments.
132
- The template is normalized to bytes, existing widgets are loaded immediately, and
133
- original metadata is captured only when `preserve_metadata` is requested.
134
- Enabling `generate_appearance_streams` also enables `need_appearances`.
137
+ The template is normalized to bytes, existing widgets are loaded immediately,
138
+ and the original metadata and title are read. A non-None `title` keyword
139
+ updates the title in the PDF stream; None preserves the title read from the
140
+ template. The deprecated `preserve_metadata` keyword is accepted for backward
141
+ compatibility and emits a deprecation warning. Enabling
142
+ `generate_appearance_streams` also enables `need_appearances`.
135
143
 
136
144
  Args:
137
145
  template (bytes | str | BinaryIO | BlankPage): The template PDF, provided as either:
@@ -148,12 +156,10 @@ class PdfWrapper:
148
156
  super().__init__()
149
157
  self._stream = fp_or_f_obj_or_stream_to_stream(template)
150
158
  self.widgets = {}
151
- self.title: Optional[str] = None
152
159
 
153
160
  self._version = None
154
- self._metadata = (
155
- get_metadata(self._read()) if kwargs.get("preserve_metadata") else {}
156
- )
161
+ self._metadata = get_metadata(self._read())
162
+ self._title = get_title(self._read())
157
163
  self._on_open_javascript = None
158
164
  self._available_fonts = {} # for setting /F1
159
165
  self._available_fonts_loaded = None # for lazy loading fonts
@@ -163,6 +169,10 @@ class PdfWrapper:
163
169
 
164
170
  # sets attrs from kwargs
165
171
  for attr, default in self.USER_PARAMS:
172
+ if attr == "preserve_metadata" and attr in kwargs:
173
+ deprecation_notice("", "preserve_metadata").emit_notice(
174
+ self, "__init__"
175
+ )
166
176
  setattr(self, attr, kwargs.get(attr, default))
167
177
 
168
178
  if getattr(self, "generate_appearance_streams") is True:
@@ -317,6 +327,34 @@ class PdfWrapper:
317
327
 
318
328
  return self
319
329
 
330
+ @property
331
+ def title(self) -> str | None:
332
+ """
333
+ Gets the title stored in the PDF's document metadata.
334
+
335
+ Returns:
336
+ str | None: The current document title, or None when no title exists.
337
+ """
338
+
339
+ return str(self._title) if self._title is not None else None
340
+
341
+ @title.setter
342
+ def title(self, value: str | None) -> None:
343
+ """
344
+ Updates the title stored in the PDF's document metadata.
345
+
346
+ A non-None value is written to the underlying PDF stream immediately.
347
+ None is ignored so the current title is preserved.
348
+
349
+ Args:
350
+ value (str | None): The new document title, or None to preserve the
351
+ current title.
352
+ """
353
+
354
+ if value is not None:
355
+ self._stream = set_title(self._read(), value)
356
+ self._title = value
357
+
320
358
  @property
321
359
  def schema(self) -> dict:
322
360
  """
@@ -430,10 +468,14 @@ class PdfWrapper:
430
468
  @property
431
469
  def on_open_javascript(self) -> str | None:
432
470
  """
433
- Returns the JavaScript script that executes when the PDF is opened.
471
+ Returns the on-open JavaScript most recently assigned to this wrapper.
472
+
473
+ This property tracks assignments made through the wrapper; it does not
474
+ inspect the template PDF for an existing `/OpenAction`.
434
475
 
435
476
  Returns:
436
- str | None: The JavaScript script, or None if no script is set.
477
+ str | None: The assigned JavaScript, or None if no script has been
478
+ assigned through this wrapper.
437
479
  """
438
480
 
439
481
  return self._on_open_javascript
@@ -443,13 +485,18 @@ class PdfWrapper:
443
485
  """
444
486
  Sets the JavaScript script that executes when the PDF is opened.
445
487
 
488
+ Assignment immediately writes a JavaScript `/OpenAction` to the stored
489
+ PDF stream, replacing any existing document-open action.
490
+
446
491
  Args:
447
492
  value (str | TextIO): The JavaScript script, provided as either:
448
493
  - str: The JavaScript code as a string, or a file path to a .js file.
449
494
  - TextIO: An open file-like object containing the JavaScript code.
450
495
  """
451
496
 
452
- self._on_open_javascript = fp_or_f_obj_or_f_content_to_content(value)
497
+ script = fp_or_f_obj_or_f_content_to_content(value)
498
+ self._stream = set_on_open_javascript(self._read(), script)
499
+ self._on_open_javascript = script
453
500
 
454
501
  def read(self) -> bytes:
455
502
  """
@@ -460,12 +507,10 @@ class PdfWrapper:
460
507
  2. If `need_appearances` is enabled, it handles appearance streams and the
461
508
  `/NeedAppearances` flag, which may include removing XFA and explicitly
462
509
  generating appearance streams.
463
- 3. If `preserve_metadata`, title, or on-open JavaScript are set, it preserves
464
- or updates the corresponding PDF properties accordingly.
465
- 4. Rebuilds the AcroForm `/Fields` array from page annotations for
510
+ 3. Rebuilds the AcroForm `/Fields` array from page annotations for
466
511
  widgets known to this wrapper, leaving the stream unchanged when no
467
512
  matching widget annotations are found.
468
- 5. Restores the wrapper's cached PDF header version after egress
513
+ 4. Restores the wrapper's cached PDF header version after egress
469
514
  processing, since PDF writers may emit their own default version.
470
515
  The wrapper's stored stream is not replaced by these final egress-only changes.
471
516
 
@@ -479,23 +524,6 @@ class PdfWrapper:
479
524
  result, getattr(self, "generate_appearance_streams")
480
525
  ) # cached
481
526
 
482
- if (
483
- any(
484
- [
485
- getattr(self, "preserve_metadata"),
486
- self.title,
487
- self.on_open_javascript,
488
- ]
489
- )
490
- and result
491
- ):
492
- result = preserve_pdf_properties(
493
- result,
494
- self.title,
495
- self.on_open_javascript,
496
- self._metadata if getattr(self, "preserve_metadata") else None,
497
- )
498
-
499
527
  if result:
500
528
  result = rebuild_acroform_fields(
501
529
  result, set(self.widgets.keys()), getattr(self, "use_full_widget_name")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 5.4.0
3
+ Version: 5.5.0
4
4
  Summary: The Python library & CLI for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -735,6 +735,10 @@ def test_blank_page(pdf_samples, request):
735
735
  expected = f.read()
736
736
 
737
737
  assert len(obj.read()) == len(expected)
738
+
739
+ assert not obj.widgets
740
+ assert len(obj.pages) == 1
741
+
738
742
  request.config.results["skip_regenerate"] = len(obj.read()) == len(expected)
739
743
 
740
744
 
@@ -752,6 +756,10 @@ def test_blank_page_custom_size_multiply(pdf_samples, request):
752
756
  expected = f.read()
753
757
 
754
758
  assert len(obj.read()) == len(expected)
759
+
760
+ assert not obj.widgets
761
+ assert len(obj.pages) == 3
762
+
755
763
  request.config.results["skip_regenerate"] = len(obj.read()) == len(expected)
756
764
 
757
765
 
File without changes
File without changes
File without changes
File without changes
File without changes