PyPDFForm 5.1.1__tar.gz → 5.2.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.1.1 → pypdfform-5.2.1}/PKG-INFO +2 -4
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/__init__.py +1 -1
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/common.py +7 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/create.py +9 -5
- pypdfform-5.2.1/PyPDFForm/cli/remove.py +33 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/root.py +14 -8
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/update.py +10 -3
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/__init__.py +6 -2
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/base.py +7 -2
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/link.py +7 -2
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/egress.py +1 -2
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/filler.py +10 -4
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/font.py +45 -11
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/hooks.py +39 -7
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/image.py +6 -9
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/patterns.py +39 -6
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/template.py +86 -23
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/watermark.py +16 -7
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/base.py +25 -7
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/signature.py +6 -4
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/wrapper.py +92 -20
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/PKG-INFO +2 -4
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/SOURCES.txt +1 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/requires.txt +1 -3
- {pypdfform-5.1.1 → pypdfform-5.2.1}/pyproject.toml +1 -3
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_font_widths.py +10 -3
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_functional.py +13 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_js.py +95 -95
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_use_full_widget_name.py +8 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/LICENSE +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/__init__.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/entry.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/inspect.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/schemas/__init__.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/schemas/create.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/schemas/update.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/constants.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/README.md +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/setup.cfg +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_create_widget.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_draw_elements.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_dropdown.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_need_appearances.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_paragraph.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_signature.py +0 -0
- {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_widget_attr_trigger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPDFForm
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.1
|
|
4
4
|
Summary: The Python library & CLI for PDF forms.
|
|
5
5
|
Author: Jinge Li
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,7 +24,7 @@ Classifier: Topic :: Utilities
|
|
|
24
24
|
Requires-Python: >=3.10
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
|
-
Requires-Dist: cryptography<
|
|
27
|
+
Requires-Dist: cryptography<50.0.0,>=48.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
30
|
Requires-Dist: pillow<13.0.0,>=12.2.0
|
|
@@ -34,9 +34,7 @@ Provides-Extra: cli
|
|
|
34
34
|
Requires-Dist: typer<1.0.0,>=0.26.1; extra == "cli"
|
|
35
35
|
Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "cli"
|
|
36
36
|
Provides-Extra: dev
|
|
37
|
-
Requires-Dist: black<27.0.0,>=26.5.1; extra == "dev"
|
|
38
37
|
Requires-Dist: coverage<8.0.0,>=7.14.1; extra == "dev"
|
|
39
|
-
Requires-Dist: isort<9.0.0,>=8.0.1; extra == "dev"
|
|
40
38
|
Requires-Dist: mike<3.0.0,>=2.2.0; extra == "dev"
|
|
41
39
|
Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "dev"
|
|
42
40
|
Requires-Dist: mkdocs-material<10.0.0,>=9.7.6; extra == "dev"
|
|
@@ -53,6 +53,13 @@ OPTIONAL_OUTPUT_PDF = Annotated[
|
|
|
53
53
|
),
|
|
54
54
|
]
|
|
55
55
|
FIELD_NAME = Annotated[str, typer.Option("--field", help="Form field name.")]
|
|
56
|
+
FIELD_NAMES = Annotated[
|
|
57
|
+
list[str],
|
|
58
|
+
typer.Option(
|
|
59
|
+
"--field",
|
|
60
|
+
help="Form field name. Repeat this option to select multiple fields.",
|
|
61
|
+
),
|
|
62
|
+
]
|
|
56
63
|
|
|
57
64
|
|
|
58
65
|
def json_file_option(help_text: str):
|
|
@@ -14,11 +14,15 @@ from typing import Annotated
|
|
|
14
14
|
|
|
15
15
|
import typer
|
|
16
16
|
|
|
17
|
-
from .. import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
from .. import Annotations, BlankPage, Fields, PdfArray, PdfWrapper, RawElements
|
|
18
|
+
from .common import (
|
|
19
|
+
INPUT_PDF,
|
|
20
|
+
OPTIONAL_OUTPUT_PDF,
|
|
21
|
+
REQUIRED_OUTPUT_PDF,
|
|
22
|
+
cli_bad_parameter,
|
|
23
|
+
create_elements_from_file,
|
|
24
|
+
json_file_option,
|
|
25
|
+
)
|
|
22
26
|
from .schemas.create import ANNOTATION_SCHEMA, FIELD_SCHEMA, RAW_SCHEMA
|
|
23
27
|
|
|
24
28
|
create_cli = typer.Typer(
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This module defines CLI commands for removing PDF form content.
|
|
4
|
+
|
|
5
|
+
It exposes the `remove` command group for deleting existing form fields.
|
|
6
|
+
Commands in this module load the target PDF, validate requested form field
|
|
7
|
+
names, apply the matching `PdfWrapper` operation, and write the modified PDF to
|
|
8
|
+
either the requested output path or the original file.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from .. import PdfWrapper
|
|
14
|
+
from .common import FIELD_NAMES, INPUT_PDF, OPTIONAL_OUTPUT_PDF, get_widget
|
|
15
|
+
|
|
16
|
+
remove_cli = typer.Typer(
|
|
17
|
+
context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@remove_cli.command(no_args_is_help=True)
|
|
22
|
+
def field(
|
|
23
|
+
ctx: typer.Context,
|
|
24
|
+
pdf: INPUT_PDF,
|
|
25
|
+
fields: FIELD_NAMES,
|
|
26
|
+
output: OPTIONAL_OUTPUT_PDF = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Remove form fields from a PDF."""
|
|
29
|
+
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
30
|
+
for field_name in fields:
|
|
31
|
+
get_widget(obj, field_name, "--field")
|
|
32
|
+
|
|
33
|
+
obj.remove_fields(fields).write(output or pdf)
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
"""
|
|
3
3
|
This module defines the root command-line interface for PyPDFForm.
|
|
4
4
|
|
|
5
|
-
It creates the Typer application, attaches the `create`, `inspect`,
|
|
6
|
-
command groups, and exposes top-level options shared by those
|
|
7
|
-
callback collects global flags in the Typer context so each
|
|
8
|
-
initialize `PdfWrapper` with consistent settings.
|
|
5
|
+
It creates the Typer application, attaches the `create`, `inspect`, `update`,
|
|
6
|
+
and `remove` command groups, and exposes top-level options shared by those
|
|
7
|
+
commands. The root callback collects global flags in the Typer context so each
|
|
8
|
+
subcommand can initialize `PdfWrapper` with consistent settings.
|
|
9
9
|
|
|
10
10
|
Commands:
|
|
11
11
|
- `fill`: Fill an existing PDF form from JSON data.
|
|
12
12
|
- `create`: Create PDFs, fields, annotations, raw elements, and grid views.
|
|
13
13
|
- `inspect`: Print form metadata and field data as JSON.
|
|
14
14
|
- `update`: Modify PDF metadata, field names, properties, geometry, and scripts.
|
|
15
|
+
- `remove`: Remove PDF form fields.
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
18
|
from pathlib import Path
|
|
@@ -20,10 +21,10 @@ from typing import Annotated
|
|
|
20
21
|
import typer
|
|
21
22
|
|
|
22
23
|
from .. import PdfWrapper, Widgets, __version__
|
|
23
|
-
from .common import
|
|
24
|
-
load_json_file)
|
|
24
|
+
from .common import INPUT_PDF, OPTIONAL_OUTPUT_PDF, json_file_option, load_json_file
|
|
25
25
|
from .create import create_cli
|
|
26
26
|
from .inspect import inspect_cli
|
|
27
|
+
from .remove import remove_cli
|
|
27
28
|
from .update import update_cli
|
|
28
29
|
|
|
29
30
|
cli_app = typer.Typer(
|
|
@@ -44,6 +45,11 @@ cli_app.add_typer(
|
|
|
44
45
|
name="update",
|
|
45
46
|
help="Update PDF metadata, fields, and scripts.",
|
|
46
47
|
)
|
|
48
|
+
cli_app.add_typer(
|
|
49
|
+
remove_cli,
|
|
50
|
+
name="remove",
|
|
51
|
+
help="Remove PDF form content.",
|
|
52
|
+
)
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
def version_callback(value: bool) -> None:
|
|
@@ -68,7 +74,7 @@ def version_callback(value: bool) -> None:
|
|
|
68
74
|
|
|
69
75
|
@cli_app.callback(
|
|
70
76
|
invoke_without_command=True,
|
|
71
|
-
help="
|
|
77
|
+
help="Work with PDF forms from the command line.",
|
|
72
78
|
)
|
|
73
79
|
def main(
|
|
74
80
|
ctx: typer.Context,
|
|
@@ -111,7 +117,7 @@ def main(
|
|
|
111
117
|
),
|
|
112
118
|
] = False,
|
|
113
119
|
) -> None:
|
|
114
|
-
"""
|
|
120
|
+
"""Work with PDF forms from the command line."""
|
|
115
121
|
ctx.obj = {
|
|
116
122
|
"need_appearances": need_appearances,
|
|
117
123
|
"generate_appearance_streams": generate_appearance_streams,
|
|
@@ -17,9 +17,16 @@ import typer
|
|
|
17
17
|
|
|
18
18
|
from .. import PdfWrapper
|
|
19
19
|
from ..lib.constants import PdfVersion
|
|
20
|
-
from .common import (
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
from .common import (
|
|
21
|
+
FIELD_NAME,
|
|
22
|
+
INPUT_PDF,
|
|
23
|
+
OPTIONAL_OUTPUT_PDF,
|
|
24
|
+
cli_bad_parameter,
|
|
25
|
+
get_widget,
|
|
26
|
+
handle_font_registration,
|
|
27
|
+
json_file_option,
|
|
28
|
+
load_json_file,
|
|
29
|
+
)
|
|
23
30
|
from .schemas.update import FIELD_SCHEMA, RENAME_SCHEMA
|
|
24
31
|
|
|
25
32
|
update_cli = typer.Typer(
|
|
@@ -14,8 +14,12 @@ from dataclasses import dataclass
|
|
|
14
14
|
from .link import LinkAnnotation
|
|
15
15
|
from .stamp import RubberStampAnnotation
|
|
16
16
|
from .text import TextAnnotation
|
|
17
|
-
from .text_markup import (
|
|
18
|
-
|
|
17
|
+
from .text_markup import (
|
|
18
|
+
HighlightAnnotation,
|
|
19
|
+
SquigglyAnnotation,
|
|
20
|
+
StrikeOutAnnotation,
|
|
21
|
+
UnderlineAnnotation,
|
|
22
|
+
)
|
|
19
23
|
|
|
20
24
|
AnnotationTypes = (
|
|
21
25
|
TextAnnotation
|
|
@@ -11,8 +11,13 @@ Classes:
|
|
|
11
11
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
|
|
14
|
-
from pypdf.generic import (
|
|
15
|
-
|
|
14
|
+
from pypdf.generic import (
|
|
15
|
+
ArrayObject,
|
|
16
|
+
DictionaryObject,
|
|
17
|
+
FloatObject,
|
|
18
|
+
NameObject,
|
|
19
|
+
TextStringObject,
|
|
20
|
+
)
|
|
16
21
|
|
|
17
22
|
from ..constants import Annot, Contents, Rect, Subtype, Type
|
|
18
23
|
|
|
@@ -12,8 +12,13 @@ Classes:
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from typing import Optional
|
|
14
14
|
|
|
15
|
-
from pypdf.generic import (
|
|
16
|
-
|
|
15
|
+
from pypdf.generic import (
|
|
16
|
+
ArrayObject,
|
|
17
|
+
DictionaryObject,
|
|
18
|
+
NameObject,
|
|
19
|
+
NumberObject,
|
|
20
|
+
TextStringObject,
|
|
21
|
+
)
|
|
17
22
|
|
|
18
23
|
from ..constants import A, S
|
|
19
24
|
from .base import Annotation
|
|
@@ -17,8 +17,7 @@ from pikepdf import Pdf
|
|
|
17
17
|
from pypdf import PdfReader, PdfWriter
|
|
18
18
|
from pypdf.generic import DictionaryObject, NameObject, TextStringObject
|
|
19
19
|
|
|
20
|
-
from .constants import
|
|
21
|
-
Title)
|
|
20
|
+
from .constants import JS, XFA, AcroForm, JavaScript, OpenAction, Root, S, Title
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
@lru_cache
|
|
@@ -24,9 +24,13 @@ from .middleware.image import Image
|
|
|
24
24
|
from .middleware.radio import Radio
|
|
25
25
|
from .middleware.signature import Signature
|
|
26
26
|
from .middleware.text import Text
|
|
27
|
-
from .patterns import (
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
from .patterns import (
|
|
28
|
+
get_widget_key,
|
|
29
|
+
update_checkbox_value,
|
|
30
|
+
update_dropdown_value,
|
|
31
|
+
update_radio_value,
|
|
32
|
+
update_text_value,
|
|
33
|
+
)
|
|
30
34
|
from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
|
|
31
35
|
|
|
32
36
|
|
|
@@ -53,7 +57,9 @@ def signature_image_handler(
|
|
|
53
57
|
any_image_to_draw = False
|
|
54
58
|
if stream is not None:
|
|
55
59
|
any_image_to_draw = True
|
|
56
|
-
image_width, image_height =
|
|
60
|
+
image_width, image_height = (
|
|
61
|
+
get_image_dimensions(stream) if middleware.preserve_aspect_ratio else (0, 0)
|
|
62
|
+
)
|
|
57
63
|
x, y, width, height = get_draw_image_resolutions(
|
|
58
64
|
widget, middleware.preserve_aspect_ratio, image_width, image_height
|
|
59
65
|
)
|
|
@@ -16,20 +16,52 @@ from zlib import compress
|
|
|
16
16
|
|
|
17
17
|
from fontTools.ttLib import TTFont as FT_TTFont
|
|
18
18
|
from pypdf import PdfReader, PdfWriter
|
|
19
|
-
from pypdf.generic import (
|
|
20
|
-
|
|
19
|
+
from pypdf.generic import (
|
|
20
|
+
ArrayObject,
|
|
21
|
+
DictionaryObject,
|
|
22
|
+
FloatObject,
|
|
23
|
+
NameObject,
|
|
24
|
+
NumberObject,
|
|
25
|
+
StreamObject,
|
|
26
|
+
)
|
|
21
27
|
from reportlab.pdfbase.pdfmetrics import _fonts
|
|
22
28
|
from reportlab.pdfbase.ttfonts import TTFError, TTFont
|
|
23
29
|
|
|
24
30
|
from .assets.blank import BlankPage
|
|
25
|
-
from .constants import (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
from .constants import (
|
|
32
|
+
DEFAULT_ASSUMED_GLYPH_WIDTH,
|
|
33
|
+
DR,
|
|
34
|
+
EM_TO_PDF_FACTOR,
|
|
35
|
+
ENCODING_TABLE_SIZE,
|
|
36
|
+
FIRST_CHAR_CODE,
|
|
37
|
+
FONT_NAME_PREFIX,
|
|
38
|
+
LAST_CHAR_CODE,
|
|
39
|
+
AcroForm,
|
|
40
|
+
BaseFont,
|
|
41
|
+
Encoding,
|
|
42
|
+
Fields,
|
|
43
|
+
Filter,
|
|
44
|
+
FirstChar,
|
|
45
|
+
FlateDecode,
|
|
46
|
+
Font,
|
|
47
|
+
FontCmap,
|
|
48
|
+
FontDescriptor,
|
|
49
|
+
FontFile2,
|
|
50
|
+
FontHead,
|
|
51
|
+
FontHmtx,
|
|
52
|
+
FontName,
|
|
53
|
+
FontNotdef,
|
|
54
|
+
LastChar,
|
|
55
|
+
Length,
|
|
56
|
+
Length1,
|
|
57
|
+
MissingWidth,
|
|
58
|
+
Resources,
|
|
59
|
+
Subtype,
|
|
60
|
+
TrueType,
|
|
61
|
+
Type,
|
|
62
|
+
Widths,
|
|
63
|
+
WinAnsiEncoding,
|
|
64
|
+
)
|
|
33
65
|
from .raw.text import RawText
|
|
34
66
|
from .watermark import create_watermarks_and_draw
|
|
35
67
|
|
|
@@ -287,7 +319,9 @@ def register_font_acroform(
|
|
|
287
319
|
font_dict_ref = writer._add_object(font_dict) # type: ignore # noqa: SLF001 # # pylint: disable=W0212
|
|
288
320
|
|
|
289
321
|
if AcroForm not in writer._root_object: # type: ignore # noqa: SLF001 # # pylint: disable=W0212
|
|
290
|
-
writer._root_object[NameObject(AcroForm)] = DictionaryObject(
|
|
322
|
+
writer._root_object[NameObject(AcroForm)] = DictionaryObject( # type: ignore # noqa: SLF001 # pylint: disable=W0212
|
|
323
|
+
{NameObject(Fields): ArrayObject([])}
|
|
324
|
+
)
|
|
291
325
|
acroform = writer._root_object[AcroForm] # type: ignore # noqa: SLF001 # # pylint: disable=W0212
|
|
292
326
|
|
|
293
327
|
if DR not in acroform:
|
|
@@ -14,15 +14,47 @@ from io import BytesIO
|
|
|
14
14
|
from typing import TextIO, cast
|
|
15
15
|
|
|
16
16
|
from pypdf import PdfReader, PdfWriter
|
|
17
|
-
from pypdf.generic import (
|
|
18
|
-
|
|
17
|
+
from pypdf.generic import (
|
|
18
|
+
ArrayObject,
|
|
19
|
+
DictionaryObject,
|
|
20
|
+
FloatObject,
|
|
21
|
+
NameObject,
|
|
22
|
+
NumberObject,
|
|
23
|
+
TextStringObject,
|
|
24
|
+
)
|
|
19
25
|
|
|
20
26
|
from .adapter import fp_or_f_obj_or_f_content_to_content
|
|
21
|
-
from .constants import (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
from .constants import (
|
|
28
|
+
AA,
|
|
29
|
+
COMB,
|
|
30
|
+
DA,
|
|
31
|
+
FONT_COLOR_IDENTIFIER,
|
|
32
|
+
FONT_SIZE_IDENTIFIER,
|
|
33
|
+
HIDDEN,
|
|
34
|
+
JS,
|
|
35
|
+
MULTILINE,
|
|
36
|
+
READ_ONLY,
|
|
37
|
+
REQUIRED,
|
|
38
|
+
TU,
|
|
39
|
+
Action,
|
|
40
|
+
Annots,
|
|
41
|
+
Bl,
|
|
42
|
+
D,
|
|
43
|
+
E,
|
|
44
|
+
F,
|
|
45
|
+
Ff,
|
|
46
|
+
Fo,
|
|
47
|
+
JavaScript,
|
|
48
|
+
MaxLen,
|
|
49
|
+
Opt,
|
|
50
|
+
Parent,
|
|
51
|
+
Q,
|
|
52
|
+
Rect,
|
|
53
|
+
S,
|
|
54
|
+
Type,
|
|
55
|
+
U,
|
|
56
|
+
X,
|
|
57
|
+
)
|
|
26
58
|
from .patterns import get_widget_key
|
|
27
59
|
|
|
28
60
|
|
|
@@ -7,6 +7,7 @@ calculating the resolutions for drawing an image on a PDF page, taking into
|
|
|
7
7
|
account whether to preserve the aspect ratio.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
from functools import lru_cache
|
|
10
11
|
from io import BytesIO
|
|
11
12
|
from typing import Tuple
|
|
12
13
|
|
|
@@ -50,12 +51,13 @@ def rotate_image(image_stream: bytes, rotation: float | int) -> bytes:
|
|
|
50
51
|
return result
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
@lru_cache
|
|
53
55
|
def get_image_dimensions(image_stream: bytes) -> Tuple[float, float]:
|
|
54
56
|
"""
|
|
55
57
|
Retrieves the width and height of an image from its byte stream.
|
|
56
58
|
|
|
57
|
-
This function uses the PIL library to open the image from the provided
|
|
58
|
-
and returns its dimensions (width and height) as a tuple of floats.
|
|
59
|
+
This cached function uses the PIL library to open the image from the provided
|
|
60
|
+
byte stream and returns its dimensions (width and height) as a tuple of floats.
|
|
59
61
|
|
|
60
62
|
Args:
|
|
61
63
|
image_stream (bytes): The image data as bytes.
|
|
@@ -63,13 +65,8 @@ def get_image_dimensions(image_stream: bytes) -> Tuple[float, float]:
|
|
|
63
65
|
Returns:
|
|
64
66
|
Tuple[float, float]: The width and height of the image in pixels.
|
|
65
67
|
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
buff.seek(0)
|
|
69
|
-
|
|
70
|
-
image = Image.open(buff)
|
|
71
|
-
|
|
72
|
-
return image.size
|
|
68
|
+
with Image.open(BytesIO(image_stream)) as image:
|
|
69
|
+
return image.size
|
|
73
70
|
|
|
74
71
|
|
|
75
72
|
def get_draw_image_resolutions(
|
|
@@ -10,12 +10,45 @@ for updating these widgets.
|
|
|
10
10
|
|
|
11
11
|
from typing import Tuple
|
|
12
12
|
|
|
13
|
-
from pypdf.generic import (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
from pypdf.generic import (
|
|
14
|
+
ArrayObject,
|
|
15
|
+
DictionaryObject,
|
|
16
|
+
NameObject,
|
|
17
|
+
NumberObject,
|
|
18
|
+
TextStringObject,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .constants import (
|
|
22
|
+
AP,
|
|
23
|
+
AS,
|
|
24
|
+
DV,
|
|
25
|
+
FT,
|
|
26
|
+
HIDDEN,
|
|
27
|
+
IMAGE_FIELD_IDENTIFIER,
|
|
28
|
+
JS,
|
|
29
|
+
SLASH,
|
|
30
|
+
TU,
|
|
31
|
+
A,
|
|
32
|
+
Btn,
|
|
33
|
+
Ch,
|
|
34
|
+
F,
|
|
35
|
+
Ff,
|
|
36
|
+
I,
|
|
37
|
+
MaxLen,
|
|
38
|
+
N,
|
|
39
|
+
Off,
|
|
40
|
+
Opt,
|
|
41
|
+
Parent,
|
|
42
|
+
Q,
|
|
43
|
+
Rect,
|
|
44
|
+
Sig,
|
|
45
|
+
Subtype,
|
|
46
|
+
T,
|
|
47
|
+
Tx,
|
|
48
|
+
V,
|
|
49
|
+
Widget,
|
|
50
|
+
Yes,
|
|
51
|
+
)
|
|
19
52
|
from .middleware.checkbox import Checkbox
|
|
20
53
|
from .middleware.dropdown import Dropdown
|
|
21
54
|
from .middleware.image import Image
|
|
@@ -22,12 +22,22 @@ from .middleware.checkbox import Checkbox
|
|
|
22
22
|
from .middleware.dropdown import Dropdown
|
|
23
23
|
from .middleware.radio import Radio
|
|
24
24
|
from .middleware.text import Text
|
|
25
|
-
from .patterns import (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
from .patterns import (
|
|
26
|
+
WIDGET_DESCRIPTION_PATTERNS,
|
|
27
|
+
WIDGET_TYPE_PATTERNS,
|
|
28
|
+
check_field_flag,
|
|
29
|
+
get_checkbox_value,
|
|
30
|
+
get_dropdown_choices,
|
|
31
|
+
get_dropdown_value,
|
|
32
|
+
get_field_hidden,
|
|
33
|
+
get_field_rect,
|
|
34
|
+
get_radio_value,
|
|
35
|
+
get_text_field_alignment,
|
|
36
|
+
get_text_field_max_length,
|
|
37
|
+
get_text_value,
|
|
38
|
+
get_widget_key,
|
|
39
|
+
update_annotation_name,
|
|
40
|
+
)
|
|
31
41
|
from .utils import extract_widget_property, find_pattern_match
|
|
32
42
|
|
|
33
43
|
|
|
@@ -366,6 +376,51 @@ def create_annotations(
|
|
|
366
376
|
return f.read()
|
|
367
377
|
|
|
368
378
|
|
|
379
|
+
def remove_widgets_by_keys(
|
|
380
|
+
pdf: bytes, keys: List[str], use_full_widget_name: bool = False
|
|
381
|
+
) -> bytes:
|
|
382
|
+
"""
|
|
383
|
+
Removes specific widgets from a PDF by their keys.
|
|
384
|
+
|
|
385
|
+
This function removes any widget annotation whose key matches one of the
|
|
386
|
+
provided keys. If no keys are provided, the original PDF stream is returned
|
|
387
|
+
unchanged.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
pdf (bytes): The PDF stream to remove widgets from.
|
|
391
|
+
keys (List[str]): A list of widget keys to remove.
|
|
392
|
+
use_full_widget_name (bool): Whether to match widgets by their full
|
|
393
|
+
names, including parent names.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
bytes: The updated PDF stream with the matching widgets removed.
|
|
397
|
+
"""
|
|
398
|
+
if not keys:
|
|
399
|
+
return pdf
|
|
400
|
+
|
|
401
|
+
writer = PdfWriter(BytesIO(pdf))
|
|
402
|
+
|
|
403
|
+
for page in writer.pages:
|
|
404
|
+
needs_update = False
|
|
405
|
+
page_annots = ArrayObject([])
|
|
406
|
+
|
|
407
|
+
for annot in page.get(Annots, []):
|
|
408
|
+
annot = cast(DictionaryObject, annot.get_object())
|
|
409
|
+
key = get_widget_key(annot.get_object(), use_full_widget_name)
|
|
410
|
+
if key not in keys:
|
|
411
|
+
page_annots.append(annot)
|
|
412
|
+
else:
|
|
413
|
+
needs_update = True
|
|
414
|
+
|
|
415
|
+
if needs_update:
|
|
416
|
+
page[NameObject(Annots)] = page_annots
|
|
417
|
+
|
|
418
|
+
with BytesIO() as f:
|
|
419
|
+
writer.write(f)
|
|
420
|
+
f.seek(0)
|
|
421
|
+
return f.read()
|
|
422
|
+
|
|
423
|
+
|
|
369
424
|
def update_widget_keys(
|
|
370
425
|
template: bytes,
|
|
371
426
|
widgets: Dict[str, WIDGET_TYPES],
|
|
@@ -394,8 +449,7 @@ def update_widget_keys(
|
|
|
394
449
|
out = PdfWriter()
|
|
395
450
|
out.append(pdf)
|
|
396
451
|
|
|
397
|
-
|
|
398
|
-
_update_single_widget_key(out, widgets, old_key, new_keys[i], indices[i])
|
|
452
|
+
_apply_widget_key_updates(out, widgets, old_keys, new_keys, indices)
|
|
399
453
|
|
|
400
454
|
with BytesIO() as f:
|
|
401
455
|
out.write(f)
|
|
@@ -403,35 +457,44 @@ def update_widget_keys(
|
|
|
403
457
|
return f.read()
|
|
404
458
|
|
|
405
459
|
|
|
406
|
-
def
|
|
460
|
+
def _apply_widget_key_updates(
|
|
407
461
|
writer: PdfWriter,
|
|
408
462
|
widgets: Dict[str, WIDGET_TYPES],
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
463
|
+
old_keys: List[str],
|
|
464
|
+
new_keys: List[str],
|
|
465
|
+
indices: List[int],
|
|
412
466
|
) -> None:
|
|
413
467
|
"""
|
|
414
|
-
|
|
468
|
+
Applies queued widget key updates to matching annotations.
|
|
469
|
+
|
|
470
|
+
The update queue is converted into a lookup keyed by old widget name, then
|
|
471
|
+
each annotation is checked against that lookup as pages are traversed.
|
|
472
|
+
Non-radio widgets honor the requested occurrence index, while radio widgets
|
|
473
|
+
update every annotation in the radio group.
|
|
415
474
|
|
|
416
475
|
Args:
|
|
417
476
|
writer (PdfWriter): The PDF writer object.
|
|
418
477
|
widgets (Dict[str, WIDGET_TYPES]): A dictionary of widgets in the template.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
478
|
+
old_keys (List[str]): The old widget keys to replace.
|
|
479
|
+
new_keys (List[str]): The new widget keys to apply.
|
|
480
|
+
indices (List[int]): Widget occurrence indices for duplicate field names.
|
|
422
481
|
"""
|
|
423
|
-
|
|
482
|
+
updates = {old_key: (new_keys[i], indices[i]) for i, old_key in enumerate(old_keys)}
|
|
483
|
+
trackers = {}
|
|
484
|
+
|
|
424
485
|
for page in writer.pages:
|
|
425
486
|
for annot in page.get(Annots, []):
|
|
426
487
|
annot = cast(DictionaryObject, annot.get_object())
|
|
427
488
|
key = get_widget_key(annot.get_object(), False)
|
|
428
489
|
|
|
429
|
-
|
|
430
|
-
if widget is None or old_key != key:
|
|
490
|
+
if key not in updates:
|
|
431
491
|
continue
|
|
432
492
|
|
|
433
|
-
|
|
434
|
-
if
|
|
435
|
-
|
|
493
|
+
widget = widgets.get(key)
|
|
494
|
+
if widget is not None:
|
|
495
|
+
trackers[key] = trackers.get(key, -1) + 1
|
|
496
|
+
new_key, index = updates[key]
|
|
497
|
+
if not isinstance(widget, Radio) and trackers[key] != index:
|
|
498
|
+
continue
|
|
436
499
|
|
|
437
|
-
|
|
500
|
+
update_annotation_name(annot, new_key)
|