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.
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PKG-INFO +1 -1
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/__init__.py +1 -1
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/inspect.py +26 -4
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/root.py +3 -1
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/egress.py +4 -55
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/template.py +128 -10
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/wrapper.py +67 -38
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/PKG-INFO +1 -1
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_functional.py +8 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/LICENSE +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/common.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/create.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/entry.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/remove.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/schemas/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/schemas/create.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/schemas/update.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/cli/update.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/constants.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/font.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/hooks.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/patterns.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/watermark.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/base.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/signature.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/SOURCES.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/requires.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/README.md +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/pyproject.toml +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/setup.cfg +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_create_widget.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_draw_elements.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_dropdown.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_font_widths.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_js.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_need_appearances.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_paragraph.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_signature.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_use_full_widget_name.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.1}/tests/test_widget_attr_trigger.py +0 -0
|
@@ -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
|
|
6
|
-
current form values, generated sample data, and field rectangle
|
|
7
|
-
Each command wraps read-only `PdfWrapper` properties so users can
|
|
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):
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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):
|
|
111
|
-
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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")
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|