PyPDFForm 5.4.1__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.
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PKG-INFO +1 -1
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/__init__.py +1 -1
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/inspect.py +26 -4
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/root.py +3 -1
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/egress.py +4 -55
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/template.py +98 -6
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/wrapper.py +63 -35
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm.egg-info/PKG-INFO +1 -1
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_functional.py +8 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/LICENSE +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/common.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/create.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/entry.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/remove.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/schemas/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/schemas/create.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/schemas/update.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/cli/update.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/constants.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/font.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/hooks.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/patterns.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/watermark.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/base.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/signature.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm.egg-info/SOURCES.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm.egg-info/requires.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/README.md +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/pyproject.toml +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/setup.cfg +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_create_widget.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_draw_elements.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_dropdown.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_font_widths.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_js.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_need_appearances.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_paragraph.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_signature.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/tests/test_use_full_widget_name.py +0 -0
- {pypdfform-5.4.1 → pypdfform-5.5.0}/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
|
|
@@ -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,
|
|
@@ -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):
|
|
111
|
-
|
|
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,
|
|
133
|
-
original metadata
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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")
|
|
@@ -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
|