PyPDFForm 5.2.0__tar.gz → 5.2.2__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.2.0 → pypdfform-5.2.2}/PKG-INFO +10 -4
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/__init__.py +1 -1
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/egress.py +5 -2
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/filler.py +3 -1
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/font.py +19 -5
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/image.py +8 -13
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/patterns.py +2 -1
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/text.py +2 -2
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/template.py +56 -26
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/types.py +1 -1
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/utils.py +1 -1
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/watermark.py +40 -29
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/base.py +30 -13
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/signature.py +9 -6
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/wrapper.py +61 -19
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/PKG-INFO +10 -4
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/requires.txt +2 -2
- {pypdfform-5.2.0 → pypdfform-5.2.2}/README.md +7 -1
- {pypdfform-5.2.0 → pypdfform-5.2.2}/pyproject.toml +2 -2
- {pypdfform-5.2.0 → pypdfform-5.2.2}/LICENSE +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/__init__.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/common.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/create.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/entry.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/inspect.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/remove.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/root.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/__init__.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/create.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/update.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/update.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/constants.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/hooks.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/SOURCES.txt +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/setup.cfg +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_create_widget.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_draw_elements.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_dropdown.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_font_widths.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_functional.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_js.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_need_appearances.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_paragraph.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_signature.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_use_full_widget_name.py +0 -0
- {pypdfform-5.2.0 → pypdfform-5.2.2}/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.2.
|
|
3
|
+
Version: 5.2.2
|
|
4
4
|
Summary: The Python library & CLI for PDF forms.
|
|
5
5
|
Author: Jinge Li
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,12 +24,12 @@ 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
|
|
31
31
|
Requires-Dist: pypdf<7.0.0,>=6.12.2
|
|
32
|
-
Requires-Dist: reportlab<
|
|
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"
|
|
35
35
|
Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "cli"
|
|
@@ -125,4 +125,10 @@ The official documentation can be found on [the GitHub page](https://chinapandam
|
|
|
125
125
|
|
|
126
126
|
This project is maintained entirely in my spare time. If you like the project please consider starring the GitHub repository. It is the best way to keep me motivated and continue making the project better.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
<a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
|
|
129
|
+
<picture>
|
|
130
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
|
|
131
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
132
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
133
|
+
</picture>
|
|
134
|
+
</a>
|
|
@@ -12,6 +12,7 @@ called right before the final PDF byte stream is returned by the wrapper module.
|
|
|
12
12
|
|
|
13
13
|
from functools import lru_cache
|
|
14
14
|
from io import BytesIO
|
|
15
|
+
from warnings import catch_warnings, simplefilter
|
|
15
16
|
|
|
16
17
|
from pikepdf import Pdf
|
|
17
18
|
from pypdf import PdfReader, PdfWriter
|
|
@@ -20,7 +21,7 @@ from pypdf.generic import DictionaryObject, NameObject, TextStringObject
|
|
|
20
21
|
from .constants import JS, XFA, AcroForm, JavaScript, OpenAction, Root, S, Title
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
@lru_cache
|
|
24
|
+
@lru_cache(maxsize=128)
|
|
24
25
|
def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) -> bytes:
|
|
25
26
|
"""
|
|
26
27
|
Handles appearance streams and the /NeedAppearances flag for a PDF form.
|
|
@@ -57,7 +58,9 @@ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) ->
|
|
|
57
58
|
result = f.read()
|
|
58
59
|
|
|
59
60
|
if generate_appearance_streams:
|
|
60
|
-
|
|
61
|
+
# TODO: remove after fixing /Annots /Fields mismatch
|
|
62
|
+
with Pdf.open(BytesIO(result)) as f, catch_warnings():
|
|
63
|
+
simplefilter("ignore")
|
|
61
64
|
f.generate_appearance_streams()
|
|
62
65
|
with BytesIO() as r:
|
|
63
66
|
f.save(r)
|
|
@@ -57,7 +57,9 @@ def signature_image_handler(
|
|
|
57
57
|
any_image_to_draw = False
|
|
58
58
|
if stream is not None:
|
|
59
59
|
any_image_to_draw = True
|
|
60
|
-
image_width, image_height =
|
|
60
|
+
image_width, image_height = (
|
|
61
|
+
get_image_dimensions(stream) if middleware.preserve_aspect_ratio else (0, 0)
|
|
62
|
+
)
|
|
61
63
|
x, y, width, height = get_draw_image_resolutions(
|
|
62
64
|
widget, middleware.preserve_aspect_ratio, image_width, image_height
|
|
63
65
|
)
|
|
@@ -66,7 +66,7 @@ from .raw.text import RawText
|
|
|
66
66
|
from .watermark import create_watermarks_and_draw
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
@lru_cache
|
|
69
|
+
@lru_cache(maxsize=128)
|
|
70
70
|
def validate_font(font_name: str, ttf_stream: bytes) -> bool:
|
|
71
71
|
"""
|
|
72
72
|
Validates a TrueType font stream.
|
|
@@ -208,7 +208,7 @@ def temporary_font_registration(
|
|
|
208
208
|
del _fonts[rl_name]
|
|
209
209
|
|
|
210
210
|
|
|
211
|
-
@lru_cache
|
|
211
|
+
@lru_cache(maxsize=128)
|
|
212
212
|
def _get_watermark_with_font(ttf_stream: bytes) -> bytes:
|
|
213
213
|
"""
|
|
214
214
|
Creates a watermark PDF with a single space character using the specified font.
|
|
@@ -230,6 +230,20 @@ def _get_watermark_with_font(ttf_stream: bytes) -> bytes:
|
|
|
230
230
|
)[0]
|
|
231
231
|
|
|
232
232
|
|
|
233
|
+
@lru_cache(maxsize=128)
|
|
234
|
+
def _compress_ttf(ttf_stream: bytes) -> bytes:
|
|
235
|
+
"""
|
|
236
|
+
Compresses a TrueType font stream for embedding in a PDF.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
ttf_stream (bytes): The font file data in TTF format.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
bytes: The compressed font stream.
|
|
243
|
+
"""
|
|
244
|
+
return compress(ttf_stream)
|
|
245
|
+
|
|
246
|
+
|
|
233
247
|
def register_font_acroform(
|
|
234
248
|
pdf: bytes, ttf_stream: bytes, need_appearances: bool
|
|
235
249
|
) -> tuple:
|
|
@@ -266,7 +280,7 @@ def register_font_acroform(
|
|
|
266
280
|
)
|
|
267
281
|
|
|
268
282
|
font_file_stream = StreamObject()
|
|
269
|
-
compressed_ttf =
|
|
283
|
+
compressed_ttf = _compress_ttf(ttf_stream)
|
|
270
284
|
font_file_stream.set_data(compressed_ttf)
|
|
271
285
|
font_file_stream.update(
|
|
272
286
|
{
|
|
@@ -341,7 +355,7 @@ def register_font_acroform(
|
|
|
341
355
|
return f.read(), new_font_name
|
|
342
356
|
|
|
343
357
|
|
|
344
|
-
@lru_cache
|
|
358
|
+
@lru_cache(maxsize=128)
|
|
345
359
|
def _get_base_font_name(ttf_stream: bytes) -> str:
|
|
346
360
|
"""
|
|
347
361
|
Extracts the base font name from a TrueType font stream.
|
|
@@ -383,7 +397,7 @@ def _get_new_font_name(fonts: dict) -> str:
|
|
|
383
397
|
return f"{FONT_NAME_PREFIX}{n}"
|
|
384
398
|
|
|
385
399
|
|
|
386
|
-
@lru_cache
|
|
400
|
+
@lru_cache(maxsize=128)
|
|
387
401
|
def get_all_available_fonts(pdf: bytes) -> dict:
|
|
388
402
|
"""
|
|
389
403
|
Retrieves all available fonts from a PDF document's AcroForm.
|
|
@@ -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
|
|
|
@@ -15,6 +16,7 @@ from PIL import Image
|
|
|
15
16
|
from .constants import Rect
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
@lru_cache(maxsize=128)
|
|
18
20
|
def rotate_image(image_stream: bytes, rotation: float | int) -> bytes:
|
|
19
21
|
"""
|
|
20
22
|
Rotates an image by a specified angle in degrees.
|
|
@@ -32,10 +34,7 @@ def rotate_image(image_stream: bytes, rotation: float | int) -> bytes:
|
|
|
32
34
|
Returns:
|
|
33
35
|
bytes: The rotated image data as bytes.
|
|
34
36
|
"""
|
|
35
|
-
buff = BytesIO()
|
|
36
|
-
buff.write(image_stream)
|
|
37
|
-
buff.seek(0)
|
|
38
|
-
|
|
37
|
+
buff = BytesIO(image_stream)
|
|
39
38
|
image = Image.open(buff)
|
|
40
39
|
|
|
41
40
|
rotated_buff = BytesIO()
|
|
@@ -50,12 +49,13 @@ def rotate_image(image_stream: bytes, rotation: float | int) -> bytes:
|
|
|
50
49
|
return result
|
|
51
50
|
|
|
52
51
|
|
|
52
|
+
@lru_cache(maxsize=128)
|
|
53
53
|
def get_image_dimensions(image_stream: bytes) -> Tuple[float, float]:
|
|
54
54
|
"""
|
|
55
55
|
Retrieves the width and height of an image from its byte stream.
|
|
56
56
|
|
|
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.
|
|
57
|
+
This cached function uses the PIL library to open the image from the provided
|
|
58
|
+
byte stream and returns its dimensions (width and height) as a tuple of floats.
|
|
59
59
|
|
|
60
60
|
Args:
|
|
61
61
|
image_stream (bytes): The image data as bytes.
|
|
@@ -63,13 +63,8 @@ def get_image_dimensions(image_stream: bytes) -> Tuple[float, float]:
|
|
|
63
63
|
Returns:
|
|
64
64
|
Tuple[float, float]: The width and height of the image in pixels.
|
|
65
65
|
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
buff.seek(0)
|
|
69
|
-
|
|
70
|
-
image = Image.open(buff)
|
|
71
|
-
|
|
72
|
-
return image.size
|
|
66
|
+
with Image.open(BytesIO(image_stream)) as image:
|
|
67
|
+
return image.size
|
|
73
68
|
|
|
74
69
|
|
|
75
70
|
def get_draw_image_resolutions(
|
|
@@ -333,7 +333,8 @@ def get_dropdown_value(annot: DictionaryObject, widget: Dropdown) -> None:
|
|
|
333
333
|
This function determines the current value of the dropdown, considering
|
|
334
334
|
whether it's a child annotation or a top-level one. It then iterates
|
|
335
335
|
through the widget's choices to find a match and sets the widget's
|
|
336
|
-
value to the index of the matched choice
|
|
336
|
+
value to the index of the matched choice, or None when the first choice
|
|
337
|
+
is selected.
|
|
337
338
|
|
|
338
339
|
Args:
|
|
339
340
|
annot (DictionaryObject): The dropdown annotation dictionary.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""
|
|
3
|
-
Contains the RawText class, which represents a text
|
|
3
|
+
Contains the RawText class, which represents a text element
|
|
4
4
|
that can be drawn directly onto a PDF page without relying on existing form fields.
|
|
5
5
|
"""
|
|
6
6
|
|
|
@@ -38,7 +38,7 @@ class RawText:
|
|
|
38
38
|
y: The y-coordinate (vertical position) of the text.
|
|
39
39
|
font: The name of the font to use for the text (defaults to DEFAULT_FONT).
|
|
40
40
|
font_size: The size of the font (defaults to DEFAULT_FONT_SIZE).
|
|
41
|
-
font_color: The color of the text as an RGB tuple (0-
|
|
41
|
+
font_color: The color of the text as an RGB tuple (0-1 for each channel).
|
|
42
42
|
"""
|
|
43
43
|
super().__init__()
|
|
44
44
|
|
|
@@ -8,6 +8,7 @@ and defines specific patterns for identifying and constructing different
|
|
|
8
8
|
types of widgets.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
from copy import deepcopy
|
|
11
12
|
from functools import lru_cache
|
|
12
13
|
from io import BytesIO
|
|
13
14
|
from typing import Dict, List, cast
|
|
@@ -41,7 +42,7 @@ from .patterns import (
|
|
|
41
42
|
from .utils import extract_widget_property, find_pattern_match
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
@lru_cache
|
|
45
|
+
@lru_cache(maxsize=128)
|
|
45
46
|
def get_metadata(pdf: bytes) -> dict:
|
|
46
47
|
"""
|
|
47
48
|
Retrieves the metadata of a PDF.
|
|
@@ -64,13 +65,11 @@ def build_widgets(
|
|
|
64
65
|
use_full_widget_name: bool,
|
|
65
66
|
) -> Dict[str, WIDGET_TYPES]:
|
|
66
67
|
"""
|
|
67
|
-
Builds
|
|
68
|
+
Builds an independent dictionary of widgets from a PDF stream.
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
and choices (for dropdowns). The constructed widgets are stored in a dictionary
|
|
73
|
-
where the keys are the widget keys and the values are the widget objects.
|
|
70
|
+
Widget discovery and construction are cached internally, then deep-copied
|
|
71
|
+
before returning so callers can safely mutate widget attributes without
|
|
72
|
+
changing cached objects or widgets returned by other calls.
|
|
74
73
|
|
|
75
74
|
Args:
|
|
76
75
|
pdf_stream (bytes): The PDF stream to parse.
|
|
@@ -81,6 +80,28 @@ def build_widgets(
|
|
|
81
80
|
Dict[str, WIDGET_TYPES]: A dictionary of widgets, where keys are widget
|
|
82
81
|
keys and values are widget objects.
|
|
83
82
|
"""
|
|
83
|
+
return deepcopy(_build_widget_cache(pdf_stream, use_full_widget_name))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@lru_cache(maxsize=128)
|
|
87
|
+
def _build_widget_cache(
|
|
88
|
+
pdf_stream: bytes,
|
|
89
|
+
use_full_widget_name: bool,
|
|
90
|
+
) -> Dict[str, WIDGET_TYPES]:
|
|
91
|
+
"""
|
|
92
|
+
Builds and caches reusable widget objects from a PDF stream.
|
|
93
|
+
|
|
94
|
+
The cached widgets must be treated as prototypes only. Use `build_widgets`
|
|
95
|
+
to get independent copies that are safe to mutate.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
pdf_stream (bytes): The PDF stream to parse.
|
|
99
|
+
use_full_widget_name (bool): Whether to use the full widget name
|
|
100
|
+
(including parent names) as the widget key.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dict[str, WIDGET_TYPES]: Cached widget prototypes keyed by widget name.
|
|
104
|
+
"""
|
|
84
105
|
results = {}
|
|
85
106
|
|
|
86
107
|
for page_num, widgets in get_widgets_by_page(pdf_stream).items():
|
|
@@ -220,7 +241,7 @@ def _handle_radio_widget(
|
|
|
220
241
|
radio.value = radio.number_of_options - 1
|
|
221
242
|
|
|
222
243
|
|
|
223
|
-
@lru_cache()
|
|
244
|
+
@lru_cache(maxsize=128)
|
|
224
245
|
def get_widgets_by_page(pdf: bytes) -> Dict[int, List[dict]]:
|
|
225
246
|
"""
|
|
226
247
|
Retrieves widgets from a PDF stream, organized by page number.
|
|
@@ -398,6 +419,7 @@ def remove_widgets_by_keys(
|
|
|
398
419
|
if not keys:
|
|
399
420
|
return pdf
|
|
400
421
|
|
|
422
|
+
key_set = set(keys)
|
|
401
423
|
writer = PdfWriter(BytesIO(pdf))
|
|
402
424
|
|
|
403
425
|
for page in writer.pages:
|
|
@@ -407,7 +429,7 @@ def remove_widgets_by_keys(
|
|
|
407
429
|
for annot in page.get(Annots, []):
|
|
408
430
|
annot = cast(DictionaryObject, annot.get_object())
|
|
409
431
|
key = get_widget_key(annot.get_object(), use_full_widget_name)
|
|
410
|
-
if key not in
|
|
432
|
+
if key not in key_set:
|
|
411
433
|
page_annots.append(annot)
|
|
412
434
|
else:
|
|
413
435
|
needs_update = True
|
|
@@ -449,8 +471,7 @@ def update_widget_keys(
|
|
|
449
471
|
out = PdfWriter()
|
|
450
472
|
out.append(pdf)
|
|
451
473
|
|
|
452
|
-
|
|
453
|
-
_update_single_widget_key(out, widgets, old_key, new_keys[i], indices[i])
|
|
474
|
+
_apply_widget_key_updates(out, widgets, old_keys, new_keys, indices)
|
|
454
475
|
|
|
455
476
|
with BytesIO() as f:
|
|
456
477
|
out.write(f)
|
|
@@ -458,35 +479,44 @@ def update_widget_keys(
|
|
|
458
479
|
return f.read()
|
|
459
480
|
|
|
460
481
|
|
|
461
|
-
def
|
|
482
|
+
def _apply_widget_key_updates(
|
|
462
483
|
writer: PdfWriter,
|
|
463
484
|
widgets: Dict[str, WIDGET_TYPES],
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
485
|
+
old_keys: List[str],
|
|
486
|
+
new_keys: List[str],
|
|
487
|
+
indices: List[int],
|
|
467
488
|
) -> None:
|
|
468
489
|
"""
|
|
469
|
-
|
|
490
|
+
Applies queued widget key updates to matching annotations.
|
|
491
|
+
|
|
492
|
+
The update queue is converted into a lookup keyed by old widget name, then
|
|
493
|
+
each annotation is checked against that lookup as pages are traversed.
|
|
494
|
+
Non-radio widgets honor the requested occurrence index, while radio widgets
|
|
495
|
+
update every annotation in the radio group.
|
|
470
496
|
|
|
471
497
|
Args:
|
|
472
498
|
writer (PdfWriter): The PDF writer object.
|
|
473
499
|
widgets (Dict[str, WIDGET_TYPES]): A dictionary of widgets in the template.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
500
|
+
old_keys (List[str]): The old widget keys to replace.
|
|
501
|
+
new_keys (List[str]): The new widget keys to apply.
|
|
502
|
+
indices (List[int]): Widget occurrence indices for duplicate field names.
|
|
477
503
|
"""
|
|
478
|
-
|
|
504
|
+
updates = {old_key: (new_keys[i], indices[i]) for i, old_key in enumerate(old_keys)}
|
|
505
|
+
trackers = {}
|
|
506
|
+
|
|
479
507
|
for page in writer.pages:
|
|
480
508
|
for annot in page.get(Annots, []):
|
|
481
509
|
annot = cast(DictionaryObject, annot.get_object())
|
|
482
510
|
key = get_widget_key(annot.get_object(), False)
|
|
483
511
|
|
|
484
|
-
|
|
485
|
-
if widget is None or old_key != key:
|
|
512
|
+
if key not in updates:
|
|
486
513
|
continue
|
|
487
514
|
|
|
488
|
-
|
|
489
|
-
if
|
|
490
|
-
|
|
515
|
+
widget = widgets.get(key)
|
|
516
|
+
if widget is not None:
|
|
517
|
+
trackers[key] = trackers.get(key, -1) + 1
|
|
518
|
+
new_key, index = updates[key]
|
|
519
|
+
if not isinstance(widget, Radio) and trackers[key] != index:
|
|
520
|
+
continue
|
|
491
521
|
|
|
492
|
-
|
|
522
|
+
update_annotation_name(annot, new_key)
|
|
@@ -18,7 +18,7 @@ class PdfArray(list):
|
|
|
18
18
|
|
|
19
19
|
When sliced, this list automatically merges the contained PdfWrapper
|
|
20
20
|
objects using the PdfWrapper.__add__ method, returning a single
|
|
21
|
-
merged PdfWrapper object. If the slice is empty, it returns
|
|
21
|
+
merged PdfWrapper object. If the slice is empty, it returns None.
|
|
22
22
|
For non-slice indexing, it behaves like a standard list.
|
|
23
23
|
"""
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ from pypdf.generic import ArrayObject, DictionaryObject, NameObject
|
|
|
26
26
|
from .constants import SLASH, UNIQUE_SUFFIX_LENGTH, Annots
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
@lru_cache
|
|
29
|
+
@lru_cache(maxsize=128)
|
|
30
30
|
def remove_all_widgets(pdf: bytes) -> bytes:
|
|
31
31
|
"""
|
|
32
32
|
Removes all widgets (form fields) from a PDF, effectively flattening the form.
|
|
@@ -9,6 +9,7 @@ and to copy specific widgets from the watermarks to the original PDF.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from collections import defaultdict
|
|
12
|
+
from functools import lru_cache
|
|
12
13
|
from io import BytesIO
|
|
13
14
|
from typing import Any, Dict, List, Optional
|
|
14
15
|
|
|
@@ -21,6 +22,20 @@ from .constants import Annots
|
|
|
21
22
|
from .patterns import get_widget_key
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
@lru_cache(maxsize=128)
|
|
26
|
+
def _get_image_reader(image_stream: bytes) -> ImageReader:
|
|
27
|
+
"""
|
|
28
|
+
Creates a cached ReportLab image reader for an image byte stream.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
image_stream (bytes): The image data as a byte stream.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
ImageReader: The cached ReportLab image reader.
|
|
35
|
+
"""
|
|
36
|
+
return ImageReader(BytesIO(image_stream))
|
|
37
|
+
|
|
38
|
+
|
|
24
39
|
def draw_text(canvas: Canvas, **kwargs) -> None:
|
|
25
40
|
"""
|
|
26
41
|
Draws a text string on the given canvas using the specified font, size, and color.
|
|
@@ -206,12 +221,8 @@ def draw_image(canvas: Canvas, **kwargs) -> None:
|
|
|
206
221
|
width = kwargs["width"]
|
|
207
222
|
height = kwargs["height"]
|
|
208
223
|
|
|
209
|
-
image_buff = BytesIO()
|
|
210
|
-
image_buff.write(image_stream)
|
|
211
|
-
image_buff.seek(0)
|
|
212
|
-
|
|
213
224
|
canvas.drawImage(
|
|
214
|
-
|
|
225
|
+
_get_image_reader(image_stream),
|
|
215
226
|
coordinate_x,
|
|
216
227
|
coordinate_y,
|
|
217
228
|
width=width,
|
|
@@ -219,8 +230,6 @@ def draw_image(canvas: Canvas, **kwargs) -> None:
|
|
|
219
230
|
mask="auto",
|
|
220
231
|
)
|
|
221
232
|
|
|
222
|
-
image_buff.close()
|
|
223
|
-
|
|
224
233
|
|
|
225
234
|
def create_watermarks_and_draw(
|
|
226
235
|
pdf: bytes, to_draw: List[dict], font_mapping: Optional[Dict[str, str]] = None
|
|
@@ -235,8 +244,9 @@ def create_watermarks_and_draw(
|
|
|
235
244
|
Args:
|
|
236
245
|
pdf (bytes): The original PDF file as a byte stream.
|
|
237
246
|
to_draw (List[dict]): A list of drawing instructions, where each dictionary
|
|
238
|
-
must contain a "page_number" key (1-based) and a "type" key
|
|
239
|
-
|
|
247
|
+
must contain a "page_number" key (1-based) and a "type" key
|
|
248
|
+
("image", "text", "line", "rect", "circle", or "ellipse") along
|
|
249
|
+
with type-specific parameters.
|
|
240
250
|
font_mapping (Optional[Dict[str, str]]): A dictionary mapping original font names
|
|
241
251
|
to temporary unique font names used by ReportLab.
|
|
242
252
|
|
|
@@ -253,20 +263,21 @@ def create_watermarks_and_draw(
|
|
|
253
263
|
"ellipse": draw_ellipse,
|
|
254
264
|
}
|
|
255
265
|
|
|
256
|
-
result = []
|
|
257
|
-
|
|
258
266
|
page_to_to_draw = defaultdict(list)
|
|
259
267
|
for each in to_draw:
|
|
260
268
|
page_to_to_draw[each["page_number"]].append(each)
|
|
261
269
|
|
|
262
270
|
pdf_file = PdfReader(BytesIO(pdf))
|
|
271
|
+
page_count = len(pdf_file.pages)
|
|
272
|
+
result = [b""] * page_count
|
|
273
|
+
font_mapping = font_mapping or {}
|
|
263
274
|
|
|
264
|
-
for
|
|
265
|
-
elements = page_to_to_draw[
|
|
275
|
+
for page_num in range(1, page_count + 1):
|
|
276
|
+
elements = page_to_to_draw.get(page_num, [])
|
|
266
277
|
if not elements:
|
|
267
|
-
result.append(b"")
|
|
268
278
|
continue
|
|
269
279
|
|
|
280
|
+
page = pdf_file.pages[page_num - 1]
|
|
270
281
|
buff = BytesIO()
|
|
271
282
|
|
|
272
283
|
canvas = Canvas(
|
|
@@ -278,13 +289,11 @@ def create_watermarks_and_draw(
|
|
|
278
289
|
)
|
|
279
290
|
|
|
280
291
|
for element in elements:
|
|
281
|
-
type_to_func[element["type"]](
|
|
282
|
-
canvas, **element, font_mapping=font_mapping or {}
|
|
283
|
-
)
|
|
292
|
+
type_to_func[element["type"]](canvas, **element, font_mapping=font_mapping)
|
|
284
293
|
|
|
285
294
|
canvas.save()
|
|
286
295
|
buff.seek(0)
|
|
287
|
-
result
|
|
296
|
+
result[page_num - 1] = buff.read()
|
|
288
297
|
|
|
289
298
|
return result
|
|
290
299
|
|
|
@@ -325,7 +334,7 @@ def merge_watermarks_with_pdf(
|
|
|
325
334
|
def _clone_page_widgets(
|
|
326
335
|
writer: PdfWriter,
|
|
327
336
|
page: PageObject,
|
|
328
|
-
keys: Optional[
|
|
337
|
+
keys: Optional[set[str]],
|
|
329
338
|
) -> List[Any]:
|
|
330
339
|
"""
|
|
331
340
|
Clones matching widgets from a single PDF page.
|
|
@@ -333,7 +342,7 @@ def _clone_page_widgets(
|
|
|
333
342
|
Args:
|
|
334
343
|
writer (PdfWriter): The PdfWriter for cloning.
|
|
335
344
|
page (PageObject): The source PDF page object.
|
|
336
|
-
keys (Optional[
|
|
345
|
+
keys (Optional[set[str]]): Keys of widgets to clone.
|
|
337
346
|
|
|
338
347
|
Returns:
|
|
339
348
|
List[Any]: A list of cloned widget objects.
|
|
@@ -349,7 +358,7 @@ def _clone_page_widgets(
|
|
|
349
358
|
def _collect_from_single_watermark_specific_page(
|
|
350
359
|
writer: PdfWriter,
|
|
351
360
|
watermark: bytes,
|
|
352
|
-
keys: Optional[
|
|
361
|
+
keys: Optional[set[str]],
|
|
353
362
|
page_num: int,
|
|
354
363
|
) -> Dict[int, List[Any]]:
|
|
355
364
|
"""
|
|
@@ -358,7 +367,7 @@ def _collect_from_single_watermark_specific_page(
|
|
|
358
367
|
Args:
|
|
359
368
|
writer (PdfWriter): The PdfWriter for cloning.
|
|
360
369
|
watermark (bytes): The watermark PDF byte stream.
|
|
361
|
-
keys (Optional[
|
|
370
|
+
keys (Optional[set[str]]): Keys of widgets to clone.
|
|
362
371
|
page_num (int): The page index within the watermark PDF.
|
|
363
372
|
|
|
364
373
|
Returns:
|
|
@@ -376,7 +385,7 @@ def _collect_from_single_watermark_specific_page(
|
|
|
376
385
|
def _collect_from_single_watermark_1_to_1(
|
|
377
386
|
writer: PdfWriter,
|
|
378
387
|
watermark: bytes,
|
|
379
|
-
keys: Optional[
|
|
388
|
+
keys: Optional[set[str]],
|
|
380
389
|
) -> Dict[int, List[Any]]:
|
|
381
390
|
"""
|
|
382
391
|
Maps pages 1:1 between a single watermark PDF and the output PDF.
|
|
@@ -384,7 +393,7 @@ def _collect_from_single_watermark_1_to_1(
|
|
|
384
393
|
Args:
|
|
385
394
|
writer (PdfWriter): The PdfWriter for cloning.
|
|
386
395
|
watermark (bytes): The watermark PDF byte stream.
|
|
387
|
-
keys (Optional[
|
|
396
|
+
keys (Optional[set[str]]): Keys of widgets to clone.
|
|
388
397
|
|
|
389
398
|
Returns:
|
|
390
399
|
Dict[int, List[Any]]: A dictionary mapping output page indices to cloned widgets.
|
|
@@ -399,7 +408,7 @@ def _collect_from_single_watermark_1_to_1(
|
|
|
399
408
|
def _collect_from_multiple_watermarks(
|
|
400
409
|
writer: PdfWriter,
|
|
401
410
|
watermarks: List[bytes],
|
|
402
|
-
keys: Optional[
|
|
411
|
+
keys: Optional[set[str]],
|
|
403
412
|
page_num: Optional[int],
|
|
404
413
|
) -> Dict[int, List[Any]]:
|
|
405
414
|
"""
|
|
@@ -408,7 +417,7 @@ def _collect_from_multiple_watermarks(
|
|
|
408
417
|
Args:
|
|
409
418
|
writer (PdfWriter): The PdfWriter for cloning.
|
|
410
419
|
watermarks (List[bytes]): A list of watermark PDF byte streams.
|
|
411
|
-
keys (Optional[
|
|
420
|
+
keys (Optional[set[str]]): Keys of widgets to clone.
|
|
412
421
|
page_num (Optional[int]): The page index within each watermark PDF.
|
|
413
422
|
|
|
414
423
|
Returns:
|
|
@@ -443,17 +452,19 @@ def _collect_widgets_to_copy(
|
|
|
443
452
|
Returns:
|
|
444
453
|
Dict[int, List[Any]]: A dictionary mapping output page indices to lists of cloned widgets.
|
|
445
454
|
"""
|
|
455
|
+
key_set = set(keys) if keys is not None else None
|
|
456
|
+
|
|
446
457
|
if isinstance(watermarks, bytes):
|
|
447
458
|
if page_num is not None:
|
|
448
459
|
# Case: Single watermark PDF, extracting a specific page to the first output page.
|
|
449
460
|
return _collect_from_single_watermark_specific_page(
|
|
450
|
-
writer, watermarks,
|
|
461
|
+
writer, watermarks, key_set, page_num
|
|
451
462
|
)
|
|
452
463
|
# Case: Single watermark PDF, mapping pages 1:1 to output pages.
|
|
453
|
-
return _collect_from_single_watermark_1_to_1(writer, watermarks,
|
|
464
|
+
return _collect_from_single_watermark_1_to_1(writer, watermarks, key_set)
|
|
454
465
|
|
|
455
466
|
# Case: List of watermark PDFs, each corresponding to an output page.
|
|
456
|
-
return _collect_from_multiple_watermarks(writer, watermarks,
|
|
467
|
+
return _collect_from_multiple_watermarks(writer, watermarks, key_set, page_num)
|
|
457
468
|
|
|
458
469
|
|
|
459
470
|
def _apply_widgets_to_pages(
|
|
@@ -15,6 +15,7 @@ Classes:
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
+
from functools import lru_cache
|
|
18
19
|
from inspect import signature
|
|
19
20
|
from io import BytesIO
|
|
20
21
|
from typing import List, Optional
|
|
@@ -102,6 +103,28 @@ class Widget:
|
|
|
102
103
|
if each in kwargs:
|
|
103
104
|
self.hook_params.append((each, kwargs.get(each)))
|
|
104
105
|
|
|
106
|
+
@staticmethod
|
|
107
|
+
@lru_cache(maxsize=128)
|
|
108
|
+
def _get_default_field_flags(acro_form_class: type, acro_form_func: str) -> tuple:
|
|
109
|
+
"""
|
|
110
|
+
Retrieves the default field flags for a ReportLab AcroForm method.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
acro_form_class (type): The ReportLab AcroForm class.
|
|
114
|
+
acro_form_func (str): The AcroForm method name.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
tuple: The default field flags split into individual flag names.
|
|
118
|
+
"""
|
|
119
|
+
default_flags = signature(
|
|
120
|
+
getattr(acro_form_class, acro_form_func)
|
|
121
|
+
).parameters.get(fieldFlags)
|
|
122
|
+
return (
|
|
123
|
+
tuple(default_flags.default.split(" "))
|
|
124
|
+
if default_flags and default_flags.default
|
|
125
|
+
else ()
|
|
126
|
+
)
|
|
127
|
+
|
|
105
128
|
def _required_handler(self, canvas: Canvas) -> None:
|
|
106
129
|
"""
|
|
107
130
|
Handles the 'Required' flag for the widget's AcroForm field.
|
|
@@ -115,13 +138,8 @@ class Widget:
|
|
|
115
138
|
Args:
|
|
116
139
|
canvas (Canvas): The ReportLab canvas object used for PDF operations.
|
|
117
140
|
"""
|
|
118
|
-
default_flags =
|
|
119
|
-
|
|
120
|
-
).parameters.get(fieldFlags)
|
|
121
|
-
default_flags = (
|
|
122
|
-
default_flags.default.split(" ")
|
|
123
|
-
if default_flags and default_flags.default
|
|
124
|
-
else []
|
|
141
|
+
default_flags = list(
|
|
142
|
+
self._get_default_field_flags(type(canvas.acroForm), self.ACRO_FORM_FUNC)
|
|
125
143
|
)
|
|
126
144
|
|
|
127
145
|
if self.acro_form_params.get(required):
|
|
@@ -174,9 +192,9 @@ class Widget:
|
|
|
174
192
|
watermark for that page. Pages without any widgets will
|
|
175
193
|
have an empty byte string (b"").
|
|
176
194
|
"""
|
|
177
|
-
result = []
|
|
178
|
-
|
|
179
195
|
pdf = PdfReader(BytesIO(stream))
|
|
196
|
+
page_count = len(pdf.pages)
|
|
197
|
+
result = [b""] * page_count
|
|
180
198
|
|
|
181
199
|
widgets_by_page = {}
|
|
182
200
|
for widget in widgets:
|
|
@@ -184,15 +202,14 @@ class Widget:
|
|
|
184
202
|
widgets_by_page[widget.page_number] = []
|
|
185
203
|
widgets_by_page[widget.page_number].append(widget)
|
|
186
204
|
|
|
187
|
-
for
|
|
188
|
-
page_num = i + 1
|
|
205
|
+
for page_num in range(1, page_count + 1):
|
|
189
206
|
if page_num not in widgets_by_page:
|
|
190
|
-
result.append(b"")
|
|
191
207
|
continue
|
|
192
208
|
|
|
193
209
|
# Use a fresh buffer per page to avoid stale trailing bytes
|
|
194
210
|
# when the current page watermark is smaller than a previous page.
|
|
195
211
|
watermark = BytesIO()
|
|
212
|
+
page = pdf.pages[page_num - 1]
|
|
196
213
|
|
|
197
214
|
canvas = Canvas(
|
|
198
215
|
watermark,
|
|
@@ -209,7 +226,7 @@ class Widget:
|
|
|
209
226
|
canvas.showPage()
|
|
210
227
|
canvas.save()
|
|
211
228
|
watermark.seek(0)
|
|
212
|
-
result
|
|
229
|
+
result[page_num - 1] = watermark.read()
|
|
213
230
|
|
|
214
231
|
return result
|
|
215
232
|
|
|
@@ -104,13 +104,13 @@ class SignatureWidget:
|
|
|
104
104
|
List[bytes]: A list of watermark PDF streams. Each element corresponds to
|
|
105
105
|
a page in the input PDF.
|
|
106
106
|
"""
|
|
107
|
-
result = []
|
|
108
|
-
|
|
109
107
|
page_to_widgets = defaultdict(list)
|
|
110
108
|
for widget in widgets:
|
|
111
109
|
page_to_widgets[widget.page_number].append(widget)
|
|
112
110
|
|
|
113
111
|
input_pdf = PdfReader(BytesIO(stream))
|
|
112
|
+
page_count = len(input_pdf.pages)
|
|
113
|
+
result = [b""] * page_count
|
|
114
114
|
|
|
115
115
|
bedrock = PdfReader(BytesIO(BEDROCK_PDF))
|
|
116
116
|
page = bedrock.pages[0]
|
|
@@ -119,9 +119,14 @@ class SignatureWidget:
|
|
|
119
119
|
key = get_widget_key(annot.get_object(), False)
|
|
120
120
|
annot_type_to_annot[key] = annot.get_object()
|
|
121
121
|
|
|
122
|
-
for
|
|
122
|
+
for page_num in range(1, page_count + 1):
|
|
123
|
+
page_widgets = page_to_widgets.get(page_num, [])
|
|
124
|
+
if not page_widgets:
|
|
125
|
+
continue
|
|
126
|
+
|
|
123
127
|
# pylint: disable=R0801
|
|
124
128
|
watermark = BytesIO()
|
|
129
|
+
p = input_pdf.pages[page_num - 1]
|
|
125
130
|
canvas = Canvas(
|
|
126
131
|
watermark,
|
|
127
132
|
pagesize=(
|
|
@@ -135,8 +140,6 @@ class SignatureWidget:
|
|
|
135
140
|
|
|
136
141
|
out = PdfWriter(watermark)
|
|
137
142
|
|
|
138
|
-
page_widgets = page_to_widgets.get(i + 1, [])
|
|
139
|
-
|
|
140
143
|
widgets_to_copy = []
|
|
141
144
|
for widget in page_widgets:
|
|
142
145
|
widget_to_copy = annot_type_to_annot[
|
|
@@ -166,7 +169,7 @@ class SignatureWidget:
|
|
|
166
169
|
with BytesIO() as f:
|
|
167
170
|
out.write(f)
|
|
168
171
|
f.seek(0)
|
|
169
|
-
result
|
|
172
|
+
result[page_num - 1] = f.read()
|
|
170
173
|
|
|
171
174
|
return result
|
|
172
175
|
|
|
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
from collections import defaultdict
|
|
22
22
|
from dataclasses import asdict
|
|
23
|
-
from functools import
|
|
23
|
+
from functools import lru_cache
|
|
24
24
|
from os import PathLike
|
|
25
25
|
from typing import (
|
|
26
26
|
TYPE_CHECKING,
|
|
@@ -131,7 +131,7 @@ class PdfWrapper:
|
|
|
131
131
|
- str: The file path to the PDF.
|
|
132
132
|
- BinaryIO: An open file-like object containing the PDF data.
|
|
133
133
|
- BlankPage: A blank page object.
|
|
134
|
-
Defaults to an empty byte string (b"")
|
|
134
|
+
Defaults to an empty byte string (b"").
|
|
135
135
|
**kwargs: Additional keyword arguments to configure the `PdfWrapper`.
|
|
136
136
|
These arguments are used to set the user-configurable parameters defined in `USER_PARAMS`.
|
|
137
137
|
For example: `use_full_widget_name=True` or `need_appearances=False`.
|
|
@@ -146,6 +146,7 @@ class PdfWrapper:
|
|
|
146
146
|
)
|
|
147
147
|
self._on_open_javascript = None
|
|
148
148
|
self._available_fonts = {} # for setting /F1
|
|
149
|
+
self._available_fonts_loaded = False
|
|
149
150
|
self._font_register_events = [] # for reregister
|
|
150
151
|
self._key_update_tracker = {} # for update key preserve old key attrs
|
|
151
152
|
self._keys_to_update = [] # for bulk update keys
|
|
@@ -208,19 +209,21 @@ class PdfWrapper:
|
|
|
208
209
|
|
|
209
210
|
def _init_helper(self) -> None:
|
|
210
211
|
"""
|
|
211
|
-
Helper method to initialize widgets
|
|
212
|
+
Helper method to initialize widgets.
|
|
212
213
|
|
|
213
214
|
This method is called during initialization and after certain operations
|
|
214
215
|
that modify the PDF content (e.g., filling, creating widgets, updating keys).
|
|
215
|
-
It rebuilds the widget dictionary and
|
|
216
|
+
It rebuilds the widget dictionary and invalidates the lazily loaded font cache.
|
|
216
217
|
"""
|
|
217
218
|
|
|
219
|
+
stream = self._read()
|
|
220
|
+
self._available_fonts_loaded = False
|
|
218
221
|
new_widgets = (
|
|
219
222
|
build_widgets(
|
|
220
|
-
|
|
223
|
+
stream,
|
|
221
224
|
getattr(self, "use_full_widget_name"),
|
|
222
225
|
)
|
|
223
|
-
if
|
|
226
|
+
if stream
|
|
224
227
|
else {}
|
|
225
228
|
)
|
|
226
229
|
# ensure old widgets don't get overwritten
|
|
@@ -238,8 +241,44 @@ class PdfWrapper:
|
|
|
238
241
|
|
|
239
242
|
self.widgets = new_widgets
|
|
240
243
|
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
def _ensure_available_fonts_loaded(self) -> dict:
|
|
245
|
+
"""
|
|
246
|
+
Loads AcroForm fonts from the PDF stream the first time they are needed.
|
|
247
|
+
|
|
248
|
+
Custom fonts registered through `register_font` are stored in the same
|
|
249
|
+
mapping, so loading updates the existing dictionary instead of replacing it.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
dict: A mapping from font names to internal PDF font identifiers.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
if not self._available_fonts_loaded:
|
|
256
|
+
if self._stream:
|
|
257
|
+
self._available_fonts.update(**get_all_available_fonts(self._stream))
|
|
258
|
+
self._available_fonts_loaded = True
|
|
259
|
+
|
|
260
|
+
return self._available_fonts
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
@lru_cache(maxsize=128)
|
|
264
|
+
def _get_page_streams_with_widgets(stream: bytes) -> tuple[bytes, ...]:
|
|
265
|
+
"""
|
|
266
|
+
Extracts page streams while preserving the original page widgets.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
stream (bytes): The PDF stream to split into pages.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
tuple[bytes, ...]: Single-page PDF streams with widgets preserved.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
return tuple(
|
|
276
|
+
# Case: Single watermark PDF, extracting a specific page to the first output page.
|
|
277
|
+
copy_watermark_widgets(page_stream, stream, None, i)
|
|
278
|
+
for i, page_stream in enumerate(
|
|
279
|
+
get_page_streams(remove_all_widgets(stream))
|
|
280
|
+
)
|
|
281
|
+
)
|
|
243
282
|
|
|
244
283
|
def _reregister_font(self) -> PdfWrapper:
|
|
245
284
|
"""
|
|
@@ -328,13 +367,16 @@ class PdfWrapper:
|
|
|
328
367
|
"""
|
|
329
368
|
Returns a list of the names of the currently registered fonts.
|
|
330
369
|
|
|
370
|
+
Accessing this property loads AcroForm fonts from the PDF stream if
|
|
371
|
+
they have not already been loaded.
|
|
372
|
+
|
|
331
373
|
Returns:
|
|
332
374
|
list: A list of font names (str).
|
|
333
375
|
"""
|
|
334
376
|
|
|
335
|
-
return list(self.
|
|
377
|
+
return list(self._ensure_available_fonts_loaded().keys())
|
|
336
378
|
|
|
337
|
-
@
|
|
379
|
+
@property
|
|
338
380
|
def pages(self) -> Sequence[PdfWrapper]:
|
|
339
381
|
"""
|
|
340
382
|
Returns a list of `PdfWrapper` objects, each representing a single page in the PDF document.
|
|
@@ -347,11 +389,10 @@ class PdfWrapper:
|
|
|
347
389
|
|
|
348
390
|
result = [
|
|
349
391
|
self.__class__(
|
|
350
|
-
|
|
351
|
-
copy_watermark_widgets(each, self._read(), None, i),
|
|
392
|
+
each,
|
|
352
393
|
**{param: getattr(self, param) for param, _ in self.USER_PARAMS},
|
|
353
394
|
)
|
|
354
|
-
for
|
|
395
|
+
for each in self._get_page_streams_with_widgets(self._read())
|
|
355
396
|
]
|
|
356
397
|
|
|
357
398
|
# because copy_watermark_widgets and remove_all_widgets
|
|
@@ -433,15 +474,15 @@ class PdfWrapper:
|
|
|
433
474
|
"""
|
|
434
475
|
|
|
435
476
|
if any(widget.hooks_to_trigger for widget in self.widgets.values()):
|
|
477
|
+
available_fonts = self._ensure_available_fonts_loaded()
|
|
436
478
|
for widget in self.widgets.values():
|
|
437
479
|
if (
|
|
438
480
|
isinstance(widget, (Text, Dropdown))
|
|
439
|
-
and widget.font not in
|
|
440
|
-
and widget.font in
|
|
481
|
+
and widget.font not in available_fonts.values()
|
|
482
|
+
and widget.font in available_fonts
|
|
441
483
|
):
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
) # from `new_font` to `/F1`
|
|
484
|
+
# from `new_font` to `/F1`
|
|
485
|
+
widget.font = available_fonts.get(widget.font)
|
|
445
486
|
|
|
446
487
|
self._stream = trigger_widget_hooks(
|
|
447
488
|
self._stream,
|
|
@@ -776,7 +817,7 @@ class PdfWrapper:
|
|
|
776
817
|
Draws raw elements (text, images, etc.) directly onto the PDF pages.
|
|
777
818
|
|
|
778
819
|
This method is the primary mechanism for drawing non-form field content.
|
|
779
|
-
It takes a list of
|
|
820
|
+
It takes a list of raw element objects and renders them
|
|
780
821
|
onto the PDF document as watermarks.
|
|
781
822
|
|
|
782
823
|
Args:
|
|
@@ -824,6 +865,7 @@ class PdfWrapper:
|
|
|
824
865
|
ttf_file = fp_or_f_obj_or_stream_to_stream(ttf_file)
|
|
825
866
|
|
|
826
867
|
if validate_font(font_name, ttf_file) if ttf_file is not None else False:
|
|
868
|
+
self._ensure_available_fonts_loaded()
|
|
827
869
|
self._stream, new_font_name = register_font_acroform(
|
|
828
870
|
self._read(), ttf_file, getattr(self, "need_appearances")
|
|
829
871
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPDFForm
|
|
3
|
-
Version: 5.2.
|
|
3
|
+
Version: 5.2.2
|
|
4
4
|
Summary: The Python library & CLI for PDF forms.
|
|
5
5
|
Author: Jinge Li
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,12 +24,12 @@ 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
|
|
31
31
|
Requires-Dist: pypdf<7.0.0,>=6.12.2
|
|
32
|
-
Requires-Dist: reportlab<
|
|
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"
|
|
35
35
|
Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "cli"
|
|
@@ -125,4 +125,10 @@ The official documentation can be found on [the GitHub page](https://chinapandam
|
|
|
125
125
|
|
|
126
126
|
This project is maintained entirely in my spare time. If you like the project please consider starring the GitHub repository. It is the best way to keep me motivated and continue making the project better.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
<a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
|
|
129
|
+
<picture>
|
|
130
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
|
|
131
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
132
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
133
|
+
</picture>
|
|
134
|
+
</a>
|
|
@@ -77,4 +77,10 @@ The official documentation can be found on [the GitHub page](https://chinapandam
|
|
|
77
77
|
|
|
78
78
|
This project is maintained entirely in my spare time. If you like the project please consider starring the GitHub repository. It is the best way to keep me motivated and continue making the project better.
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
<a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
|
|
81
|
+
<picture>
|
|
82
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
|
|
83
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
84
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
85
|
+
</picture>
|
|
86
|
+
</a>
|
|
@@ -29,12 +29,12 @@ classifiers = [
|
|
|
29
29
|
]
|
|
30
30
|
requires-python = ">=3.10"
|
|
31
31
|
dependencies = [
|
|
32
|
-
"cryptography>=48.0.0,<
|
|
32
|
+
"cryptography>=48.0.0,<50.0.0",
|
|
33
33
|
"fonttools>=4.63.0,<5.0.0",
|
|
34
34
|
"pikepdf>=10.7.2,<11.0.0",
|
|
35
35
|
"pillow>=12.2.0,<13.0.0",
|
|
36
36
|
"pypdf>=6.12.2,<7.0.0",
|
|
37
|
-
"reportlab>=4.5.1,<
|
|
37
|
+
"reportlab>=4.5.1,<6.0.0",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
40
|
[project.urls]
|
|
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
|