PyPDFForm 6.0.1__tar.gz → 6.1.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-6.0.1 → pypdfform-6.1.0}/PKG-INFO +4 -4
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/__init__.py +2 -3
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/common.py +16 -2
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/create.py +5 -2
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/update.py +18 -7
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/adapter.py +17 -9
- pypdfform-6.1.0/PyPDFForm/lib/annotations/link.py +98 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/constants.py +22 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/deprecation.py +47 -2
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/egress.py +73 -17
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/filler.py +52 -8
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/font.py +20 -8
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/middleware/base.py +53 -1
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/middleware/checkbox.py +2 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/middleware/dropdown.py +2 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/middleware/text.py +12 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/patterns.py +10 -9
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/raw/image.py +4 -3
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/template.py +51 -8
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/utils.py +117 -13
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/watermark.py +18 -5
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/wrapper.py +158 -41
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm.egg-info/PKG-INFO +4 -4
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm.egg-info/SOURCES.txt +0 -1
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm.egg-info/requires.txt +3 -3
- {pypdfform-6.0.1 → pypdfform-6.1.0}/pyproject.toml +4 -3
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_font_related.py +97 -2
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_functional.py +591 -124
- pypdfform-6.1.0/tests/test_signature.py +458 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_widget_attr_trigger.py +61 -0
- pypdfform-6.0.1/PyPDFForm/lib/annotations/link.py +0 -68
- pypdfform-6.0.1/PyPDFForm/lib/types.py +0 -64
- pypdfform-6.0.1/tests/test_signature.py +0 -136
- {pypdfform-6.0.1 → pypdfform-6.1.0}/LICENSE +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/__init__.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/entry.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/inspect.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/remove.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/root.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/schemas/__init__.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/schemas/create.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/cli/schemas/update.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/hooks.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/widgets/base.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/widgets/signature.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/README.md +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/setup.cfg +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_create_widget.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_draw_elements.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_dropdown.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_js.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_need_appearances.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_paragraph.py +0 -0
- {pypdfform-6.0.1 → pypdfform-6.1.0}/tests/test_use_full_widget_name.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPDFForm
|
|
3
|
-
Version: 6.0
|
|
3
|
+
Version: 6.1.0
|
|
4
4
|
Summary: The Python library & CLI for PDF forms.
|
|
5
5
|
Author: Jinge Li
|
|
6
6
|
License-Expression: MIT
|
|
@@ -27,8 +27,8 @@ License-File: LICENSE
|
|
|
27
27
|
Requires-Dist: cryptography<51.0.0,>=50.0.0
|
|
28
28
|
Requires-Dist: fonttools<5.0.0,>=4.63.0
|
|
29
29
|
Requires-Dist: pikepdf<11.0.0,>=10.7.2
|
|
30
|
-
Requires-Dist: pillow<
|
|
31
|
-
Requires-Dist: pypdf<7.0.0,>=6.
|
|
30
|
+
Requires-Dist: pillow<14.0.0,>=12.2.0
|
|
31
|
+
Requires-Dist: pypdf<7.0.0,>=6.19.0
|
|
32
32
|
Requires-Dist: reportlab<6.0.0,>=4.5.1
|
|
33
33
|
Provides-Extra: cli
|
|
34
34
|
Requires-Dist: typer<1.0.0,>=0.26.1; extra == "cli"
|
|
@@ -39,7 +39,7 @@ Requires-Dist: coverage<8.0.0,>=7.14.1; extra == "dev"
|
|
|
39
39
|
Requires-Dist: mike<3.0.0,>=2.2.0; extra == "dev"
|
|
40
40
|
Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "dev"
|
|
41
41
|
Requires-Dist: mkdocs-material<10.0.0,>=9.7.6; extra == "dev"
|
|
42
|
-
Requires-Dist: pudb<
|
|
42
|
+
Requires-Dist: pudb<2027.0.0,>=2025.1.5; extra == "dev"
|
|
43
43
|
Requires-Dist: pylint<5.0.0,>=4.0.5; extra == "dev"
|
|
44
44
|
Requires-Dist: pyright<2.0.0,>=1.1.409; extra == "dev"
|
|
45
45
|
Requires-Dist: pytest<10.0.0,>=9.0.3; extra == "dev"
|
|
@@ -16,15 +16,14 @@ PyPDFForm aims to make PDF form automation straightforward whether it is used
|
|
|
16
16
|
from Python code or from the command line.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
__version__ = "6.0
|
|
19
|
+
__version__ = "6.1.0"
|
|
20
20
|
|
|
21
21
|
from .lib.annotations import Annotations
|
|
22
22
|
from .lib.assets.blank import BlankPage
|
|
23
23
|
from .lib.middleware import Widgets
|
|
24
24
|
from .lib.raw import RawElements
|
|
25
|
-
from .lib.types import PdfArray
|
|
26
25
|
from .lib.widgets import Fields
|
|
27
|
-
from .lib.wrapper import PdfWrapper
|
|
26
|
+
from .lib.wrapper import PdfArray, PdfWrapper
|
|
28
27
|
|
|
29
28
|
__all__ = [
|
|
30
29
|
"PdfWrapper",
|
|
@@ -312,6 +312,7 @@ def create_elements_from_file(
|
|
|
312
312
|
options_param_hint: str,
|
|
313
313
|
output: Path | None = None,
|
|
314
314
|
element_type: str | None = None,
|
|
315
|
+
input_errors: tuple[type[Exception], ...] = (),
|
|
315
316
|
) -> None:
|
|
316
317
|
"""
|
|
317
318
|
Creates PDF elements from a data file or command-line options.
|
|
@@ -321,7 +322,8 @@ def create_elements_from_file(
|
|
|
321
322
|
from the additional command-line options stored in ``ctx.args``. The data
|
|
322
323
|
file takes precedence when both input forms are supplied. Each definition
|
|
323
324
|
is constructed after optional font registration and passed to `method_name`
|
|
324
|
-
on `PdfWrapper`.
|
|
325
|
+
on `PdfWrapper`. Errors of the `input_errors` types raised by that method
|
|
326
|
+
are reported against the input form that was used, without writing output.
|
|
325
327
|
|
|
326
328
|
Args:
|
|
327
329
|
pdf (Path): The path to the input PDF file.
|
|
@@ -342,6 +344,9 @@ def create_elements_from_file(
|
|
|
342
344
|
omitted, the input PDF is overwritten. Defaults to None.
|
|
343
345
|
element_type (str, optional): Element type selected for command-line
|
|
344
346
|
input. Defaults to None.
|
|
347
|
+
input_errors (tuple[type[Exception], ...], optional): Exception types
|
|
348
|
+
raised by `method_name` for invalid element definitions. Defaults to
|
|
349
|
+
an empty tuple.
|
|
345
350
|
"""
|
|
346
351
|
if data is not None:
|
|
347
352
|
input_data = load_data_file(data, schema, file_param_hint)
|
|
@@ -370,4 +375,13 @@ def create_elements_from_file(
|
|
|
370
375
|
handle_font_registration(obj, each, registered_font)
|
|
371
376
|
ungrouped_input.append(element_map[k](**each))
|
|
372
377
|
|
|
373
|
-
|
|
378
|
+
try:
|
|
379
|
+
result = getattr(obj, method_name)(ungrouped_input)
|
|
380
|
+
except input_errors as exc:
|
|
381
|
+
cli_bad_parameter(
|
|
382
|
+
str(exc),
|
|
383
|
+
param_hint=file_param_hint if data is not None else options_param_hint,
|
|
384
|
+
cause=exc,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
result.write(output or pdf)
|
|
@@ -126,7 +126,8 @@ def extract(
|
|
|
126
126
|
The command validates that the requested end page does not precede the
|
|
127
127
|
start page, converts the 1-based CLI page numbers into the slice expected
|
|
128
128
|
by `PdfWrapper.pages`, and writes the extracted pages to the required
|
|
129
|
-
output path.
|
|
129
|
+
output path. Each page is extracted separately, so links to other pages
|
|
130
|
+
are removed.
|
|
130
131
|
|
|
131
132
|
Args:
|
|
132
133
|
ctx (typer.Context): Typer context containing global `PdfWrapper`
|
|
@@ -332,7 +333,8 @@ def annotation(
|
|
|
332
333
|
The command maps input groups such as `text`, `link`, and `highlight` to
|
|
333
334
|
annotation classes, validates the input against the CLI annotation schema,
|
|
334
335
|
creates the corresponding annotation objects, and calls
|
|
335
|
-
`PdfWrapper.annotate` before writing the modified PDF. A
|
|
336
|
+
`PdfWrapper.annotate` before writing the modified PDF. A link to a page
|
|
337
|
+
outside the PDF is reported as a parameter error. A data file can
|
|
336
338
|
define multiple annotations grouped by type. Without `--file`, `--type` and
|
|
337
339
|
the annotation options define one annotation. The data file takes
|
|
338
340
|
precedence when both input forms are supplied.
|
|
@@ -368,6 +370,7 @@ def annotation(
|
|
|
368
370
|
options_param_hint="annotation options",
|
|
369
371
|
output=output,
|
|
370
372
|
element_type=element_type,
|
|
373
|
+
input_errors=(ValueError,),
|
|
371
374
|
)
|
|
372
375
|
|
|
373
376
|
|
|
@@ -297,9 +297,10 @@ def field(
|
|
|
297
297
|
|
|
298
298
|
The command validates the input against the field update schema, loads the
|
|
299
299
|
PDF with the global CLI options stored in `ctx.obj`, resolves each field
|
|
300
|
-
name,
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
name, checks that each field type supports its requested properties,
|
|
301
|
+
registers any referenced fonts once per invocation, assigns the requested
|
|
302
|
+
widget properties, and writes the modified PDF to the requested output path
|
|
303
|
+
or back to the input file. A data file can update multiple
|
|
303
304
|
fields. Without `--file`, `--field` and the property options update one
|
|
304
305
|
field. The data file takes precedence when both input forms are supplied.
|
|
305
306
|
|
|
@@ -315,8 +316,9 @@ def field(
|
|
|
315
316
|
Defaults to None.
|
|
316
317
|
|
|
317
318
|
Raises:
|
|
318
|
-
typer.BadParameter: Raised when the input file is invalid
|
|
319
|
-
field does not exist
|
|
319
|
+
typer.BadParameter: Raised when the input file is invalid, a requested
|
|
320
|
+
field does not exist, or a field type does not support a requested
|
|
321
|
+
property.
|
|
320
322
|
"""
|
|
321
323
|
if data is not None:
|
|
322
324
|
input_data = load_data_file(data, FIELD_SCHEMA, "--file")
|
|
@@ -337,12 +339,21 @@ def field(
|
|
|
337
339
|
field_param_hint = "--field"
|
|
338
340
|
|
|
339
341
|
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
340
|
-
|
|
342
|
+
field_widgets = {}
|
|
341
343
|
for k, each in input_data.items():
|
|
342
344
|
field_widget = get_widget(obj, k, field_param_hint)
|
|
345
|
+
for param in each:
|
|
346
|
+
try:
|
|
347
|
+
field_widget.check_property_supported(param)
|
|
348
|
+
except AttributeError as exc:
|
|
349
|
+
cli_bad_parameter(str(exc), param_hint=field_param_hint, cause=exc)
|
|
350
|
+
field_widgets[k] = field_widget
|
|
351
|
+
|
|
352
|
+
registered_font = {}
|
|
353
|
+
for k, each in input_data.items():
|
|
343
354
|
handle_font_registration(obj, each, registered_font)
|
|
344
355
|
for param, v in each.items():
|
|
345
|
-
setattr(
|
|
356
|
+
setattr(field_widgets[k], param, v)
|
|
346
357
|
|
|
347
358
|
obj.write(output or pdf)
|
|
348
359
|
|
|
@@ -10,6 +10,7 @@ forms. The module ensures that the input is properly converted into a byte
|
|
|
10
10
|
stream before further processing.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
from os import PathLike
|
|
13
14
|
from os.path import isfile
|
|
14
15
|
from typing import Any, BinaryIO
|
|
15
16
|
|
|
@@ -32,7 +33,7 @@ def readable(obj: Any) -> bool:
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def fp_or_f_obj_or_stream_to_stream(
|
|
35
|
-
fp_or_f_obj_or_stream: bytes | str | BinaryIO,
|
|
36
|
+
fp_or_f_obj_or_stream: bytes | str | PathLike | BinaryIO,
|
|
36
37
|
) -> bytes:
|
|
37
38
|
"""
|
|
38
39
|
Adapt a file path, file object, or stream to a byte stream.
|
|
@@ -40,30 +41,37 @@ def fp_or_f_obj_or_stream_to_stream(
|
|
|
40
41
|
This function takes a file path, a file object, or a byte stream and adapts it to a consistent byte stream.
|
|
41
42
|
It handles different input types, including:
|
|
42
43
|
- byte streams (bytes)
|
|
43
|
-
- file paths (str)
|
|
44
|
+
- file paths (str or PathLike)
|
|
44
45
|
- file-like objects with a read() method (BinaryIO)
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
Paths are opened directly, so a missing path raises ``FileNotFoundError``.
|
|
47
|
+
Empty bytes and empty readable objects are returned as ``b""``.
|
|
47
48
|
|
|
48
49
|
Args:
|
|
49
|
-
fp_or_f_obj_or_stream (bytes | str | BinaryIO): The input to adapt.
|
|
50
|
-
It can be a byte stream, a file path
|
|
50
|
+
fp_or_f_obj_or_stream (bytes | str | PathLike | BinaryIO): The input to adapt.
|
|
51
|
+
It can be a byte stream, a file path, or a file object.
|
|
51
52
|
|
|
52
53
|
Returns:
|
|
53
54
|
bytes: The byte stream representation of the input.
|
|
54
|
-
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
OSError: If a file path cannot be opened, such as ``FileNotFoundError``
|
|
58
|
+
for a missing path.
|
|
59
|
+
TypeError: If the input is not bytes, a file path, or a readable object.
|
|
55
60
|
"""
|
|
56
61
|
# not cached to handle writing to the same disk file
|
|
57
|
-
result = b""
|
|
58
62
|
if isinstance(fp_or_f_obj_or_stream, bytes):
|
|
59
63
|
result = fp_or_f_obj_or_stream
|
|
60
64
|
|
|
61
65
|
elif readable(fp_or_f_obj_or_stream):
|
|
62
66
|
result = fp_or_f_obj_or_stream.read()
|
|
63
67
|
|
|
64
|
-
elif isinstance(fp_or_f_obj_or_stream, str
|
|
68
|
+
elif isinstance(fp_or_f_obj_or_stream, (str, PathLike)):
|
|
65
69
|
with open(fp_or_f_obj_or_stream, "rb") as _file:
|
|
66
70
|
result = _file.read()
|
|
71
|
+
|
|
72
|
+
else:
|
|
73
|
+
msg = f"Expected bytes, a file path, or a readable binary stream, got {type(fp_or_f_obj_or_stream).__name__}."
|
|
74
|
+
raise TypeError(msg)
|
|
67
75
|
return result
|
|
68
76
|
|
|
69
77
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This module defines the class for link annotations in PyPDFForm.
|
|
4
|
+
|
|
5
|
+
It provides a structure for representing and interacting with PDF link
|
|
6
|
+
annotations, which allow users to click and navigate to a URI or an internal page.
|
|
7
|
+
|
|
8
|
+
Classes:
|
|
9
|
+
- `LinkAnnotation`: A dataclass representing the properties of a PDF link annotation.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Optional, Sequence
|
|
14
|
+
|
|
15
|
+
from pypdf.generic import (
|
|
16
|
+
ArrayObject,
|
|
17
|
+
DictionaryObject,
|
|
18
|
+
IndirectObject,
|
|
19
|
+
NameObject,
|
|
20
|
+
TextStringObject,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from ..constants import A, Link, S
|
|
24
|
+
from .base import Annotation
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class LinkAnnotation(Annotation):
|
|
29
|
+
"""
|
|
30
|
+
A dataclass representing the properties of a PDF link annotation.
|
|
31
|
+
|
|
32
|
+
This class extends the `Annotation` base class to specifically handle
|
|
33
|
+
link annotations, including the target URI or an internal page.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
uri (str): The URI that the link annotation points to. Takes precedence
|
|
37
|
+
over `page` when both are set. Defaults to None.
|
|
38
|
+
page (int): The 1-based page number in the same PDF that the link
|
|
39
|
+
annotation points to. Defaults to None.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_annotation_type: str = Link
|
|
43
|
+
|
|
44
|
+
uri: Optional[str] = None
|
|
45
|
+
page: Optional[int] = None
|
|
46
|
+
|
|
47
|
+
def get_specific_properties(self) -> dict:
|
|
48
|
+
"""
|
|
49
|
+
Gets properties specific to the link annotation.
|
|
50
|
+
|
|
51
|
+
This method extends the base properties with a URI action. A link to
|
|
52
|
+
an internal page needs the pages of the target PDF, so its destination
|
|
53
|
+
is built separately by `get_destination`.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
dict: A dictionary of PDF properties specific to the link annotation.
|
|
57
|
+
"""
|
|
58
|
+
result = super().get_specific_properties()
|
|
59
|
+
if self.uri is not None:
|
|
60
|
+
result[NameObject(A)] = DictionaryObject(
|
|
61
|
+
{
|
|
62
|
+
NameObject(S): NameObject("/URI"),
|
|
63
|
+
NameObject("/URI"): TextStringObject(self.uri),
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def get_destination(
|
|
70
|
+
self, page_references: Sequence[IndirectObject]
|
|
71
|
+
) -> Optional[ArrayObject]:
|
|
72
|
+
"""
|
|
73
|
+
Gets the destination of a link to an internal page.
|
|
74
|
+
|
|
75
|
+
A destination in the same PDF must refer to the target page object
|
|
76
|
+
rather than a page index, which is reserved for destinations in other
|
|
77
|
+
PDFs. The 1-based `page` is resolved against the page references of the
|
|
78
|
+
PDF that will contain the link.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
page_references (Sequence[IndirectObject]): References to the pages
|
|
82
|
+
of the PDF that will contain the link, in page order.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Optional[ArrayObject]: The destination that fits the target page in
|
|
86
|
+
the window, or None if the link has a URI or no page.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If `page` is outside the pages of the PDF.
|
|
90
|
+
"""
|
|
91
|
+
if self.uri is not None or self.page is None:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
if not 1 <= self.page <= len(page_references):
|
|
95
|
+
msg = f"Link page {self.page} must satisfy 1 <= page <= {len(page_references)}."
|
|
96
|
+
raise ValueError(msg)
|
|
97
|
+
|
|
98
|
+
return ArrayObject([page_references[self.page - 1], NameObject("/Fit")])
|
|
@@ -41,10 +41,30 @@ VERSION_IDENTIFIERS = [
|
|
|
41
41
|
|
|
42
42
|
DEPRECATION_NOTICE = "{} will be deprecated soon."
|
|
43
43
|
DEPRECATION_REPLACE_NOTICE = "Use {} instead."
|
|
44
|
+
UNEXPECTED_KWARG_NOTICE = "{}() got an unexpected keyword argument '{}'."
|
|
45
|
+
REMOVED_KWARG_NOTICE = "It was removed in PyPDFForm {}. {}"
|
|
46
|
+
SUPPORTED_KWARGS_NOTICE = "Supported keyword arguments: {}."
|
|
47
|
+
# method name -> removed keyword -> (version removed, migration guidance)
|
|
48
|
+
REMOVED_KWARGS = {
|
|
49
|
+
"__init__": {
|
|
50
|
+
"preserve_metadata": (
|
|
51
|
+
"6.0.0",
|
|
52
|
+
"Remove it; input PDF metadata is preserved automatically.",
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
"fill": {
|
|
56
|
+
"flatten": (
|
|
57
|
+
"6.0.0",
|
|
58
|
+
"Use readonly=True to make fields read-only; there is no form-flattening replacement.",
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
}
|
|
44
62
|
|
|
45
63
|
Title = "/Title"
|
|
46
64
|
Annots = "/Annots"
|
|
47
65
|
Annot = "/Annot"
|
|
66
|
+
Link = "/Link"
|
|
67
|
+
Dest = "/Dest"
|
|
48
68
|
Contents = "/Contents"
|
|
49
69
|
Widget = "/Widget"
|
|
50
70
|
XObject = "/XObject"
|
|
@@ -56,6 +76,7 @@ TU = "/TU"
|
|
|
56
76
|
Rect = "/Rect"
|
|
57
77
|
FT = "/FT"
|
|
58
78
|
Parent = "/Parent"
|
|
79
|
+
Kids = "/Kids"
|
|
59
80
|
Ff = "/Ff"
|
|
60
81
|
F = "/F"
|
|
61
82
|
Tx = "/Tx"
|
|
@@ -89,6 +110,7 @@ A = "/A"
|
|
|
89
110
|
AA = "/AA"
|
|
90
111
|
Action = "/Action"
|
|
91
112
|
S = "/S"
|
|
113
|
+
GoTo = "/GoTo"
|
|
92
114
|
JavaScript = "/JavaScript"
|
|
93
115
|
JS = "/JS"
|
|
94
116
|
OpenAction = "/OpenAction"
|
|
@@ -4,13 +4,22 @@ A module for handling deprecation notices within the PyPDFForm library.
|
|
|
4
4
|
|
|
5
5
|
This module provides utility functions to issue standard DeprecationWarning
|
|
6
6
|
messages, ensuring consistency across the library when notifying users of
|
|
7
|
-
deprecated features.
|
|
7
|
+
deprecated features. It also rejects keyword arguments that are unsupported
|
|
8
|
+
or were removed after deprecation.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from functools import wraps
|
|
12
|
+
from typing import Any, Sequence
|
|
11
13
|
from warnings import warn
|
|
12
14
|
|
|
13
|
-
from .constants import
|
|
15
|
+
from .constants import (
|
|
16
|
+
DEPRECATION_NOTICE,
|
|
17
|
+
DEPRECATION_REPLACE_NOTICE,
|
|
18
|
+
REMOVED_KWARG_NOTICE,
|
|
19
|
+
REMOVED_KWARGS,
|
|
20
|
+
SUPPORTED_KWARGS_NOTICE,
|
|
21
|
+
UNEXPECTED_KWARG_NOTICE,
|
|
22
|
+
)
|
|
14
23
|
|
|
15
24
|
|
|
16
25
|
def deprecation_notice(to_replace: str, param: str = "") -> callable:
|
|
@@ -72,3 +81,39 @@ def deprecation_notice(to_replace: str, param: str = "") -> callable:
|
|
|
72
81
|
|
|
73
82
|
decorator.emit_notice = emit_notice
|
|
74
83
|
return decorator
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def reject_unsupported_kwargs(
|
|
87
|
+
obj: Any, method_name: str, kwargs: dict, supported: Sequence[str]
|
|
88
|
+
) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Raises a TypeError for the first keyword argument a method does not support.
|
|
91
|
+
|
|
92
|
+
The message names the keyword like Python's own unexpected-keyword error.
|
|
93
|
+
Keywords listed for the method in `REMOVED_KWARGS` add the version that
|
|
94
|
+
removed them and their migration guidance; other keywords list the
|
|
95
|
+
supported ones.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
obj (Any): The object whose method received the keyword arguments.
|
|
99
|
+
method_name (str): The method name, such as "__init__" or "fill".
|
|
100
|
+
kwargs (dict): The keyword arguments passed to the method.
|
|
101
|
+
supported (Sequence[str]): The keyword arguments the method supports.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
TypeError: If any keyword argument is not supported.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
name = obj.__class__.__name__
|
|
108
|
+
if method_name != "__init__":
|
|
109
|
+
name = f"{name}.{method_name}"
|
|
110
|
+
removed = REMOVED_KWARGS.get(method_name, {})
|
|
111
|
+
|
|
112
|
+
for key in kwargs:
|
|
113
|
+
if key not in supported:
|
|
114
|
+
msg = UNEXPECTED_KWARG_NOTICE.format(name, key)
|
|
115
|
+
if key in removed:
|
|
116
|
+
msg = f"{msg} {REMOVED_KWARG_NOTICE.format(*removed[key])}"
|
|
117
|
+
else:
|
|
118
|
+
msg = f"{msg} {SUPPORTED_KWARGS_NOTICE.format(', '.join(supported))}"
|
|
119
|
+
raise TypeError(msg)
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
Prepare PDF output and publish serialized PDFs to filesystem paths.
|
|
4
|
+
|
|
5
|
+
Preparation helpers manage appearance streams and the /NeedAppearances flag,
|
|
6
|
+
and rebuild the AcroForm `/Fields` array from page widget annotations before
|
|
7
|
+
`PdfWrapper.read()` returns the final bytes.
|
|
8
|
+
|
|
9
|
+
For path output, `write_path()` writes serialized bytes to a sibling temporary
|
|
10
|
+
file and closes it before atomic replacement. It follows symlinks, preserves
|
|
11
|
+
existing permission bits, and cleans up failed temporary writes.
|
|
11
12
|
"""
|
|
12
13
|
|
|
14
|
+
import os
|
|
15
|
+
import stat
|
|
16
|
+
from contextlib import suppress
|
|
13
17
|
from functools import lru_cache
|
|
14
18
|
from io import BytesIO
|
|
19
|
+
from tempfile import NamedTemporaryFile
|
|
15
20
|
from warnings import catch_warnings, filterwarnings
|
|
16
21
|
|
|
17
22
|
from pikepdf import Pdf
|
|
@@ -24,10 +29,57 @@ from .constants import (
|
|
|
24
29
|
Annots,
|
|
25
30
|
Fields,
|
|
26
31
|
Parent,
|
|
32
|
+
Subtype,
|
|
33
|
+
Widget,
|
|
27
34
|
)
|
|
28
35
|
from .template import get_widget_key
|
|
29
36
|
|
|
30
37
|
|
|
38
|
+
def write_path(dest: str | bytes | os.PathLike, content: bytes) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Write serialized content to a sibling temporary file and replace its target.
|
|
41
|
+
|
|
42
|
+
Symlinks are followed. Existing permission bits are copied before replacement;
|
|
43
|
+
new files retain the temporary file's private permissions. Other file metadata
|
|
44
|
+
and hard-link relationships are not preserved. Failures before replacement
|
|
45
|
+
leave the destination's bytes unchanged and remove the temporary file.
|
|
46
|
+
"""
|
|
47
|
+
path = os.path.realpath(os.fsdecode(dest))
|
|
48
|
+
mode = None
|
|
49
|
+
try:
|
|
50
|
+
destination_stat = os.stat(path)
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
pass
|
|
53
|
+
else:
|
|
54
|
+
if not stat.S_ISREG(destination_stat.st_mode):
|
|
55
|
+
message = "The output destination must be a regular file."
|
|
56
|
+
raise OSError(message)
|
|
57
|
+
mode = stat.S_IMODE(destination_stat.st_mode)
|
|
58
|
+
|
|
59
|
+
temporary_path = None
|
|
60
|
+
try:
|
|
61
|
+
with NamedTemporaryFile(
|
|
62
|
+
mode="wb", prefix=".pypdfform-", dir=os.path.dirname(path), delete=False
|
|
63
|
+
) as output:
|
|
64
|
+
temporary_path = output.name
|
|
65
|
+
if output.write(content) != len(content):
|
|
66
|
+
message = "The temporary PDF write was incomplete."
|
|
67
|
+
raise OSError(message)
|
|
68
|
+
|
|
69
|
+
if mode is not None:
|
|
70
|
+
os.chmod(temporary_path, mode)
|
|
71
|
+
os.replace(temporary_path, path)
|
|
72
|
+
finally:
|
|
73
|
+
if temporary_path is not None:
|
|
74
|
+
with suppress(FileNotFoundError):
|
|
75
|
+
try:
|
|
76
|
+
os.unlink(temporary_path)
|
|
77
|
+
except PermissionError:
|
|
78
|
+
# Windows cannot unlink a temporary file made read-only above.
|
|
79
|
+
os.chmod(temporary_path, stat.S_IRUSR | stat.S_IWUSR)
|
|
80
|
+
os.unlink(temporary_path)
|
|
81
|
+
|
|
82
|
+
|
|
31
83
|
@lru_cache(maxsize=8)
|
|
32
84
|
def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) -> bytes:
|
|
33
85
|
"""
|
|
@@ -85,8 +137,8 @@ def rebuild_acroform_fields(
|
|
|
85
137
|
Rebuilds the AcroForm `/Fields` array from matching page annotations.
|
|
86
138
|
|
|
87
139
|
The existing `/Fields` array is replaced, creating an AcroForm dictionary
|
|
88
|
-
when necessary.
|
|
89
|
-
|
|
140
|
+
when necessary. Only widget annotations are resolved to a widget key, and
|
|
141
|
+
those whose keys are present in `widget_keys` contribute their
|
|
90
142
|
top-level field object to the new array. Page annotation arrays are left
|
|
91
143
|
unchanged. When no matching page annotations are found, the original PDF
|
|
92
144
|
stream is returned unchanged to avoid an unnecessary rewrite.
|
|
@@ -111,13 +163,17 @@ def rebuild_acroform_fields(
|
|
|
111
163
|
seen_fields = set()
|
|
112
164
|
for page in writer.pages:
|
|
113
165
|
for annot in page.get(Annots, []):
|
|
114
|
-
|
|
115
|
-
if
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
166
|
+
annotation = annot.get_object()
|
|
167
|
+
if (
|
|
168
|
+
annotation.get(Subtype) != Widget
|
|
169
|
+
or get_widget_key(annotation, use_full_widget_name) not in widget_keys
|
|
170
|
+
):
|
|
171
|
+
continue
|
|
172
|
+
field_ref = _get_root_field_reference(writer, annot)
|
|
173
|
+
field_key = _field_reference_key(field_ref)
|
|
174
|
+
if field_key not in seen_fields:
|
|
175
|
+
fields.append(field_ref)
|
|
176
|
+
seen_fields.add(field_key)
|
|
121
177
|
|
|
122
178
|
if not seen_fields:
|
|
123
179
|
return pdf
|
|
@@ -12,9 +12,9 @@ from io import BytesIO
|
|
|
12
12
|
from typing import Dict, cast
|
|
13
13
|
|
|
14
14
|
from pypdf import PdfWriter
|
|
15
|
-
from pypdf.generic import DictionaryObject
|
|
15
|
+
from pypdf.generic import ArrayObject, DictionaryObject
|
|
16
16
|
|
|
17
|
-
from .constants import
|
|
17
|
+
from .constants import AcroForm, Fields, Kids, Parent, Subtype, Widget
|
|
18
18
|
from .hooks import update_field_readonly
|
|
19
19
|
from .image import get_draw_image_resolutions, get_image_dimensions
|
|
20
20
|
from .middleware import WIDGET_TYPES
|
|
@@ -90,7 +90,8 @@ def update_widget(
|
|
|
90
90
|
|
|
91
91
|
This function updates the value of a single PDF form widget based on its type. It sets
|
|
92
92
|
the read-only flag first when requested, skips value updates when the
|
|
93
|
-
middleware value is ``None``, tracks radio option indices within each group,
|
|
93
|
+
middleware value is ``None``, tracks radio option indices within each group,
|
|
94
|
+
updates the entire radio group when its selected option is reached, and
|
|
94
95
|
prepares images or signatures for later drawing instead of writing image data directly
|
|
95
96
|
into the annotation.
|
|
96
97
|
|
|
@@ -156,6 +157,36 @@ def handle_image_drawing(
|
|
|
156
157
|
return merge_watermarks_with_pdf(result, create_watermarks_and_draw(result, images))
|
|
157
158
|
|
|
158
159
|
|
|
160
|
+
def _detach_consumed_widget(writer: PdfWriter, annot: DictionaryObject) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Detaches a consumed widget and empty ancestor fields from the AcroForm tree.
|
|
163
|
+
|
|
164
|
+
This function removes the widget from its parent's child array or the
|
|
165
|
+
AcroForm's root field array. It removes ancestors that become empty, stopping
|
|
166
|
+
at the first parent with remaining children. Resolved objects are compared
|
|
167
|
+
by identity so unrelated fields with the same name retain their references.
|
|
168
|
+
The caller is responsible for removing the widget from page annotations.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
writer (PdfWriter): The PDF writer that owns the widget and field tree.
|
|
172
|
+
annot (DictionaryObject): The consumed image or signature widget to detach.
|
|
173
|
+
"""
|
|
174
|
+
field = annot
|
|
175
|
+
while Parent in field:
|
|
176
|
+
parent = cast(DictionaryObject, field[Parent])
|
|
177
|
+
children = cast(ArrayObject, parent.get(Kids, ArrayObject()).get_object())
|
|
178
|
+
children[:] = [child for child in children if child.get_object() is not field]
|
|
179
|
+
if children:
|
|
180
|
+
return
|
|
181
|
+
field = parent
|
|
182
|
+
|
|
183
|
+
acroform = writer.root_object.get(AcroForm, DictionaryObject()).get_object()
|
|
184
|
+
fields = cast(ArrayObject, acroform.get(Fields, ArrayObject()).get_object())
|
|
185
|
+
fields[:] = [
|
|
186
|
+
reference for reference in fields if reference.get_object() is not field
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
|
|
159
190
|
def fill(
|
|
160
191
|
template: bytes,
|
|
161
192
|
widgets: Dict[str, WIDGET_TYPES],
|
|
@@ -169,8 +200,9 @@ def fill(
|
|
|
169
200
|
widgets on each page of the PDF and updates their values based on the provided `widgets`
|
|
170
201
|
dictionary. The function supports text fields, checkboxes, radio buttons, dropdowns,
|
|
171
202
|
images, and signatures. It can set fields read-only during filling, and when images or
|
|
172
|
-
signatures are present it
|
|
173
|
-
|
|
203
|
+
signatures are present it removes only the widgets whose images were prepared,
|
|
204
|
+
including their field-tree entries, and returns a second stream with the image
|
|
205
|
+
watermarks applied. All other annotations retain their existing references.
|
|
174
206
|
|
|
175
207
|
Args:
|
|
176
208
|
template (bytes): The PDF template as bytes.
|
|
@@ -196,15 +228,21 @@ def fill(
|
|
|
196
228
|
|
|
197
229
|
for page_num, page in enumerate(out.pages):
|
|
198
230
|
images_to_draw[page_num + 1] = []
|
|
199
|
-
|
|
200
|
-
|
|
231
|
+
annotations = page.annotations
|
|
232
|
+
if annotations is None:
|
|
233
|
+
continue
|
|
234
|
+
# Keep a snapshot because consumed widgets are removed during iteration.
|
|
235
|
+
for reference in annotations.copy():
|
|
236
|
+
annot = cast(DictionaryObject, reference.get_object())
|
|
237
|
+
if annot.get(Subtype) != Widget:
|
|
238
|
+
continue
|
|
201
239
|
key = get_widget_key(annot.get_object(), use_full_widget_name)
|
|
202
240
|
|
|
203
241
|
widget = widgets.get(key)
|
|
204
242
|
if widget is None:
|
|
205
243
|
continue
|
|
206
244
|
|
|
207
|
-
|
|
245
|
+
consumed = update_widget(
|
|
208
246
|
annot,
|
|
209
247
|
widget,
|
|
210
248
|
key,
|
|
@@ -213,6 +251,12 @@ def fill(
|
|
|
213
251
|
need_appearances,
|
|
214
252
|
readonly,
|
|
215
253
|
)
|
|
254
|
+
if consumed:
|
|
255
|
+
any_image_to_draw = True
|
|
256
|
+
annotations[:] = [
|
|
257
|
+
item for item in annotations if item.get_object() is not annot
|
|
258
|
+
]
|
|
259
|
+
_detach_consumed_widget(out, annot)
|
|
216
260
|
|
|
217
261
|
with BytesIO() as f:
|
|
218
262
|
out.write(f)
|