PyPDFForm 5.4.1__tar.gz → 5.5.1__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.1 → pypdfform-5.5.1}/PKG-INFO +1 -1
  2. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/inspect.py +26 -4
  4. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/root.py +3 -1
  5. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/egress.py +4 -55
  6. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/template.py +128 -10
  7. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/wrapper.py +67 -38
  8. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/PKG-INFO +1 -1
  9. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_functional.py +8 -0
  10. {pypdfform-5.4.1 → pypdfform-5.5.1}/LICENSE +0 -0
  11. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/__init__.py +0 -0
  12. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/common.py +0 -0
  13. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/create.py +0 -0
  14. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/entry.py +0 -0
  15. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/remove.py +0 -0
  16. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/schemas/__init__.py +0 -0
  17. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/schemas/create.py +0 -0
  18. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/schemas/update.py +0 -0
  19. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/update.py +0 -0
  20. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/__init__.py +0 -0
  21. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/adapter.py +0 -0
  22. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/__init__.py +0 -0
  23. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/base.py +0 -0
  24. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/link.py +0 -0
  25. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/stamp.py +0 -0
  26. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/text.py +0 -0
  27. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  28. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/assets/__init__.py +0 -0
  29. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/assets/bedrock.py +0 -0
  30. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/assets/blank.py +0 -0
  31. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/constants.py +0 -0
  32. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/coordinate.py +0 -0
  33. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/deprecation.py +0 -0
  34. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/filler.py +0 -0
  35. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/font.py +0 -0
  36. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/hooks.py +0 -0
  37. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/image.py +0 -0
  38. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/__init__.py +0 -0
  39. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/base.py +0 -0
  40. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  41. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  42. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/image.py +0 -0
  43. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/radio.py +0 -0
  44. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/signature.py +0 -0
  45. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/text.py +0 -0
  46. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/patterns.py +0 -0
  47. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/__init__.py +0 -0
  48. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/circle.py +0 -0
  49. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/ellipse.py +0 -0
  50. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/image.py +0 -0
  51. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/line.py +0 -0
  52. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/rect.py +0 -0
  53. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/text.py +0 -0
  54. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/types.py +0 -0
  55. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/utils.py +0 -0
  56. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/watermark.py +0 -0
  57. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/__init__.py +0 -0
  58. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/base.py +0 -0
  59. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  60. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  61. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/image.py +0 -0
  62. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/radio.py +0 -0
  63. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/signature.py +0 -0
  64. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/text.py +0 -0
  65. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  66. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  67. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/entry_points.txt +0 -0
  68. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/requires.txt +0 -0
  69. {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/top_level.txt +0 -0
  70. {pypdfform-5.4.1 → pypdfform-5.5.1}/README.md +0 -0
  71. {pypdfform-5.4.1 → pypdfform-5.5.1}/pyproject.toml +0 -0
  72. {pypdfform-5.4.1 → pypdfform-5.5.1}/setup.cfg +0 -0
  73. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_bulk_create_fields.py +0 -0
  74. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_create_widget.py +0 -0
  75. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_draw_elements.py +0 -0
  76. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_dropdown.py +0 -0
  77. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_extract_middleware_attributes.py +0 -0
  78. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_fill_max_length_text_field.py +0 -0
  79. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_font_widths.py +0 -0
  80. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_generate_appearance_streams.py +0 -0
  81. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_js.py +0 -0
  82. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_need_appearances.py +0 -0
  83. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_paragraph.py +0 -0
  84. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_signature.py +0 -0
  85. {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_use_full_widget_name.py +0 -0
  86. {pypdfform-5.4.1 → pypdfform-5.5.1}/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.1
3
+ Version: 5.5.1
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.1"
21
+ __version__ = "5.5.1"
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
  """
@@ -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
@@ -18,19 +16,14 @@ from warnings import catch_warnings, filterwarnings
18
16
 
19
17
  from pikepdf import Pdf
20
18
  from pypdf import PdfWriter
21
- from pypdf.generic import ArrayObject, DictionaryObject, NameObject, TextStringObject
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
- S,
33
- Title,
34
27
  )
35
28
  from .template import get_widget_key
36
29
 
@@ -85,50 +78,6 @@ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) ->
85
78
  return result
86
79
 
87
80
 
88
- def preserve_pdf_properties(
89
- pdf: bytes, title: str, script: str, metadata: dict
90
- ) -> bytes:
91
- """
92
- Preserves and updates PDF properties such as metadata, title, and OpenAction scripts.
93
-
94
- This function allows setting or updating the PDF's title and metadata, and
95
- attaching a JavaScript script that executes when the PDF is opened. Metadata
96
- is merged into the reader's current metadata when provided; the title and
97
- OpenAction JavaScript are written only when non-empty values are supplied.
98
-
99
- Args:
100
- pdf (bytes): The PDF file content as a bytes stream.
101
- title (str): The title to be set in the PDF metadata.
102
- script (str): JavaScript code to be executed when the PDF is opened.
103
- metadata (dict): The original metadata to preserve.
104
-
105
- Returns:
106
- bytes: The modified PDF content as a bytes stream.
107
- """
108
- writer = PdfWriter(BytesIO(pdf))
109
-
110
- if title or metadata:
111
- _metadata = writer.metadata or {}
112
- if metadata:
113
- _metadata.update(metadata)
114
- if title:
115
- _metadata[NameObject(Title)] = TextStringObject(title)
116
-
117
- writer.add_metadata(_metadata)
118
-
119
- if script:
120
- open_action = DictionaryObject()
121
- open_action[NameObject(S)] = NameObject(JavaScript)
122
- open_action[NameObject(JS)] = TextStringObject(script)
123
-
124
- writer._root_object.update({NameObject(OpenAction): open_action}) # type: ignore # noqa: SLF001 # # pylint: disable=W0212
125
-
126
- with BytesIO() as f:
127
- writer.write(f)
128
- f.seek(0)
129
- return f.read()
130
-
131
-
132
81
  def rebuild_acroform_fields(
133
82
  pdf: bytes, widget_keys: set, use_full_widget_name: bool
134
83
  ) -> bytes:
@@ -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
@@ -53,11 +64,118 @@ def get_metadata(pdf: bytes) -> dict:
53
64
  Returns:
54
65
  dict: A dictionary containing the PDF's metadata.
55
66
  """
56
- if not pdf:
57
- return {}
67
+ result = {}
68
+ if pdf:
69
+ reader = PdfReader(BytesIO(pdf))
70
+ result = reader.metadata or {}
71
+
72
+ return result
73
+
74
+
75
+ def get_title(pdf: bytes) -> str | None:
76
+ """
77
+ Retrieves the title from a PDF's document metadata.
78
+
79
+ Args:
80
+ pdf (bytes): The PDF stream from which to retrieve the title.
81
+
82
+ Returns:
83
+ str | None: The document title, or None when no title is present.
84
+ """
85
+ return get_metadata(pdf).get(Title)
86
+
87
+
88
+ @lru_cache(maxsize=128)
89
+ def get_on_open_javascript(pdf: bytes) -> str | None:
90
+ """
91
+ Retrieves the JavaScript configured to run when a PDF is opened.
92
+
93
+ Results are cached by PDF stream. Only a document-catalog `/OpenAction`
94
+ whose action type is JavaScript is returned.
95
+
96
+ Args:
97
+ pdf (bytes): The PDF stream to inspect for a document-open action.
98
+
99
+ Returns:
100
+ str | None: The JavaScript source, or None when the stream is empty or
101
+ does not contain a JavaScript document-open action.
102
+ """
103
+ result = None
104
+ if pdf:
105
+ reader = PdfReader(BytesIO(pdf))
106
+ root_object = reader.root_object
107
+ if OpenAction in root_object and root_object[OpenAction].get(S) == JavaScript:
108
+ result = root_object[OpenAction].get(JS)
109
+
110
+ return result
111
+
112
+
113
+ def set_metadata(pdf: bytes, metadata: dict) -> bytes:
114
+ """
115
+ Merges metadata into a PDF's existing document metadata.
116
+
117
+ Existing entries are retained unless the supplied mapping contains the
118
+ same key, in which case the supplied value replaces the existing value.
119
+
120
+ Args:
121
+ pdf (bytes): The PDF stream whose metadata should be updated.
122
+ metadata (dict): Metadata entries to add or replace.
123
+
124
+ Returns:
125
+ bytes: The PDF stream containing the merged metadata.
126
+ """
127
+ writer = PdfWriter(BytesIO(pdf))
128
+ _metadata = writer.metadata or {}
129
+ _metadata.update(metadata)
130
+ writer.add_metadata(_metadata)
131
+
132
+ with BytesIO() as f:
133
+ writer.write(f)
134
+ f.seek(0)
135
+ return f.read()
136
+
137
+
138
+ def set_title(pdf: bytes, title: str) -> bytes:
139
+ """
140
+ Sets a PDF's document title while preserving its other metadata.
141
+
142
+ Args:
143
+ pdf (bytes): The PDF stream whose title should be set.
144
+ title (str): The document title to store in the PDF metadata.
145
+
146
+ Returns:
147
+ bytes: The PDF stream containing the updated title.
148
+ """
149
+ return set_metadata(pdf, {Title: title})
58
150
 
59
- reader = PdfReader(BytesIO(pdf))
60
- return reader.metadata or {}
151
+
152
+ def set_on_open_javascript(pdf: bytes, script: str) -> bytes:
153
+ """
154
+ Sets a PDF's document-open action to execute JavaScript.
155
+
156
+ The script is written to the document catalog as a JavaScript
157
+ `/OpenAction`, replacing any existing document-open action. An action is
158
+ written even when `script` is empty.
159
+
160
+ Args:
161
+ pdf (bytes): The PDF stream whose document-open action should be set.
162
+ script (str): JavaScript to execute when the PDF is opened.
163
+
164
+ Returns:
165
+ bytes: The PDF stream containing the JavaScript `/OpenAction`.
166
+ """
167
+ writer = PdfWriter(BytesIO(pdf))
168
+
169
+ open_action = DictionaryObject()
170
+ open_action[NameObject(S)] = NameObject(JavaScript)
171
+ open_action[NameObject(JS)] = TextStringObject(script)
172
+
173
+ writer._root_object.update({NameObject(OpenAction): open_action}) # type: ignore # noqa: SLF001 # # pylint: disable=W0212
174
+
175
+ with BytesIO() as f:
176
+ writer.write(f)
177
+ f.seek(0)
178
+ return f.read()
61
179
 
62
180
 
63
181
  def build_widgets(
@@ -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
@@ -57,8 +56,11 @@ from .middleware.text import Text
57
56
  from .template import (
58
57
  build_widgets,
59
58
  create_annotations,
60
- get_metadata,
59
+ get_on_open_javascript,
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 preserved 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,13 @@ 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 and existing widgets are loaded immediately.
138
+ The title and document-open JavaScript remain in the PDF stream and are read
139
+ lazily when their properties are accessed. A non-None `title` keyword updates
140
+ the title in the PDF stream; None leaves the template's title unchanged. The
141
+ deprecated `preserve_metadata` keyword is accepted for backward compatibility
142
+ and emits a deprecation warning. Enabling
143
+ `generate_appearance_streams` also enables `need_appearances`.
135
144
 
136
145
  Args:
137
146
  template (bytes | str | BinaryIO | BlankPage): The template PDF, provided as either:
@@ -148,13 +157,8 @@ class PdfWrapper:
148
157
  super().__init__()
149
158
  self._stream = fp_or_f_obj_or_stream_to_stream(template)
150
159
  self.widgets = {}
151
- self.title: Optional[str] = None
152
160
 
153
161
  self._version = None
154
- self._metadata = (
155
- get_metadata(self._read()) if kwargs.get("preserve_metadata") else {}
156
- )
157
- self._on_open_javascript = None
158
162
  self._available_fonts = {} # for setting /F1
159
163
  self._available_fonts_loaded = None # for lazy loading fonts
160
164
  self._font_register_events = [] # for reregister
@@ -163,6 +167,10 @@ class PdfWrapper:
163
167
 
164
168
  # sets attrs from kwargs
165
169
  for attr, default in self.USER_PARAMS:
170
+ if attr == "preserve_metadata" and attr in kwargs:
171
+ deprecation_notice("", "preserve_metadata").emit_notice(
172
+ self, "__init__"
173
+ )
166
174
  setattr(self, attr, kwargs.get(attr, default))
167
175
 
168
176
  if getattr(self, "generate_appearance_streams") is True:
@@ -317,6 +325,37 @@ class PdfWrapper:
317
325
 
318
326
  return self
319
327
 
328
+ @property
329
+ def title(self) -> str | None:
330
+ """
331
+ Gets the title stored in the PDF's document metadata.
332
+
333
+ The title is read lazily from the current PDF stream. Metadata extraction
334
+ is cached for each distinct stream.
335
+
336
+ Returns:
337
+ str | None: The current document title, or None when no title exists.
338
+ """
339
+
340
+ result = get_title(self._read())
341
+ return str(result) if result is not None else None
342
+
343
+ @title.setter
344
+ def title(self, value: str | None) -> None:
345
+ """
346
+ Updates the title stored in the PDF's document metadata.
347
+
348
+ A non-None value is written to the underlying PDF stream immediately.
349
+ None is ignored so the current title is preserved.
350
+
351
+ Args:
352
+ value (str | None): The new document title, or None to preserve the
353
+ current title.
354
+ """
355
+
356
+ if value is not None:
357
+ self._stream = set_title(self._read(), value)
358
+
320
359
  @property
321
360
  def schema(self) -> dict:
322
361
  """
@@ -430,26 +469,35 @@ class PdfWrapper:
430
469
  @property
431
470
  def on_open_javascript(self) -> str | None:
432
471
  """
433
- Returns the JavaScript script that executes when the PDF is opened.
472
+ Gets the JavaScript stored in the PDF's document-open action.
473
+
474
+ The script is read lazily from the current PDF stream, including an existing
475
+ JavaScript `/OpenAction` in the input template. Extraction is cached for each
476
+ distinct stream.
434
477
 
435
478
  Returns:
436
- str | None: The JavaScript script, or None if no script is set.
479
+ str | None: The current document-open JavaScript, or None when the PDF
480
+ does not contain a JavaScript document-open action.
437
481
  """
438
482
 
439
- return self._on_open_javascript
483
+ return get_on_open_javascript(self._read())
440
484
 
441
485
  @on_open_javascript.setter
442
486
  def on_open_javascript(self, value: str | TextIO) -> None:
443
487
  """
444
488
  Sets the JavaScript script that executes when the PDF is opened.
445
489
 
490
+ Assignment immediately writes a JavaScript `/OpenAction` to the stored
491
+ PDF stream, replacing any existing document-open action.
492
+
446
493
  Args:
447
494
  value (str | TextIO): The JavaScript script, provided as either:
448
495
  - str: The JavaScript code as a string, or a file path to a .js file.
449
496
  - TextIO: An open file-like object containing the JavaScript code.
450
497
  """
451
498
 
452
- self._on_open_javascript = fp_or_f_obj_or_f_content_to_content(value)
499
+ script = fp_or_f_obj_or_f_content_to_content(value)
500
+ self._stream = set_on_open_javascript(self._read(), script)
453
501
 
454
502
  def read(self) -> bytes:
455
503
  """
@@ -460,12 +508,10 @@ class PdfWrapper:
460
508
  2. If `need_appearances` is enabled, it handles appearance streams and the
461
509
  `/NeedAppearances` flag, which may include removing XFA and explicitly
462
510
  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
511
+ 3. Rebuilds the AcroForm `/Fields` array from page annotations for
466
512
  widgets known to this wrapper, leaving the stream unchanged when no
467
513
  matching widget annotations are found.
468
- 5. Restores the wrapper's cached PDF header version after egress
514
+ 4. Restores the wrapper's cached PDF header version after egress
469
515
  processing, since PDF writers may emit their own default version.
470
516
  The wrapper's stored stream is not replaced by these final egress-only changes.
471
517
 
@@ -479,23 +525,6 @@ class PdfWrapper:
479
525
  result, getattr(self, "generate_appearance_streams")
480
526
  ) # cached
481
527
 
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
528
  if result:
500
529
  result = rebuild_acroform_fields(
501
530
  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.1
3
+ Version: 5.5.1
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