PyPDFForm 5.2.1__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.1 → pypdfform-5.2.2}/PKG-INFO +9 -3
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/__init__.py +1 -1
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/egress.py +5 -2
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/font.py +19 -5
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/image.py +3 -5
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/patterns.py +2 -1
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/text.py +2 -2
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/template.py +31 -9
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/types.py +1 -1
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/utils.py +1 -1
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/watermark.py +24 -22
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/base.py +6 -7
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/signature.py +6 -6
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/wrapper.py +37 -15
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/PKG-INFO +9 -3
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/requires.txt +1 -1
- {pypdfform-5.2.1 → pypdfform-5.2.2}/README.md +7 -1
- {pypdfform-5.2.1 → pypdfform-5.2.2}/pyproject.toml +1 -1
- {pypdfform-5.2.1 → pypdfform-5.2.2}/LICENSE +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/__init__.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/common.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/create.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/entry.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/inspect.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/remove.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/root.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/__init__.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/create.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/update.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/update.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/constants.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/hooks.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/SOURCES.txt +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/setup.cfg +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_create_widget.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_draw_elements.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_dropdown.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_font_widths.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_functional.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_js.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_need_appearances.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_paragraph.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_signature.py +0 -0
- {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_use_full_widget_name.py +0 -0
- {pypdfform-5.2.1 → 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
|
|
@@ -29,7 +29,7 @@ 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)
|
|
@@ -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.
|
|
@@ -16,6 +16,7 @@ from PIL import Image
|
|
|
16
16
|
from .constants import Rect
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
@lru_cache(maxsize=128)
|
|
19
20
|
def rotate_image(image_stream: bytes, rotation: float | int) -> bytes:
|
|
20
21
|
"""
|
|
21
22
|
Rotates an image by a specified angle in degrees.
|
|
@@ -33,10 +34,7 @@ def rotate_image(image_stream: bytes, rotation: float | int) -> bytes:
|
|
|
33
34
|
Returns:
|
|
34
35
|
bytes: The rotated image data as bytes.
|
|
35
36
|
"""
|
|
36
|
-
buff = BytesIO()
|
|
37
|
-
buff.write(image_stream)
|
|
38
|
-
buff.seek(0)
|
|
39
|
-
|
|
37
|
+
buff = BytesIO(image_stream)
|
|
40
38
|
image = Image.open(buff)
|
|
41
39
|
|
|
42
40
|
rotated_buff = BytesIO()
|
|
@@ -51,7 +49,7 @@ def rotate_image(image_stream: bytes, rotation: float | int) -> bytes:
|
|
|
51
49
|
return result
|
|
52
50
|
|
|
53
51
|
|
|
54
|
-
@lru_cache
|
|
52
|
+
@lru_cache(maxsize=128)
|
|
55
53
|
def get_image_dimensions(image_stream: bytes) -> Tuple[float, float]:
|
|
56
54
|
"""
|
|
57
55
|
Retrieves the width and height of an image from its byte stream.
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -244,8 +244,9 @@ def create_watermarks_and_draw(
|
|
|
244
244
|
Args:
|
|
245
245
|
pdf (bytes): The original PDF file as a byte stream.
|
|
246
246
|
to_draw (List[dict]): A list of drawing instructions, where each dictionary
|
|
247
|
-
must contain a "page_number" key (1-based) and a "type" key
|
|
248
|
-
|
|
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.
|
|
249
250
|
font_mapping (Optional[Dict[str, str]]): A dictionary mapping original font names
|
|
250
251
|
to temporary unique font names used by ReportLab.
|
|
251
252
|
|
|
@@ -262,20 +263,21 @@ def create_watermarks_and_draw(
|
|
|
262
263
|
"ellipse": draw_ellipse,
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
result = []
|
|
266
|
-
|
|
267
266
|
page_to_to_draw = defaultdict(list)
|
|
268
267
|
for each in to_draw:
|
|
269
268
|
page_to_to_draw[each["page_number"]].append(each)
|
|
270
269
|
|
|
271
270
|
pdf_file = PdfReader(BytesIO(pdf))
|
|
271
|
+
page_count = len(pdf_file.pages)
|
|
272
|
+
result = [b""] * page_count
|
|
273
|
+
font_mapping = font_mapping or {}
|
|
272
274
|
|
|
273
|
-
for
|
|
274
|
-
elements = page_to_to_draw[
|
|
275
|
+
for page_num in range(1, page_count + 1):
|
|
276
|
+
elements = page_to_to_draw.get(page_num, [])
|
|
275
277
|
if not elements:
|
|
276
|
-
result.append(b"")
|
|
277
278
|
continue
|
|
278
279
|
|
|
280
|
+
page = pdf_file.pages[page_num - 1]
|
|
279
281
|
buff = BytesIO()
|
|
280
282
|
|
|
281
283
|
canvas = Canvas(
|
|
@@ -287,13 +289,11 @@ def create_watermarks_and_draw(
|
|
|
287
289
|
)
|
|
288
290
|
|
|
289
291
|
for element in elements:
|
|
290
|
-
type_to_func[element["type"]](
|
|
291
|
-
canvas, **element, font_mapping=font_mapping or {}
|
|
292
|
-
)
|
|
292
|
+
type_to_func[element["type"]](canvas, **element, font_mapping=font_mapping)
|
|
293
293
|
|
|
294
294
|
canvas.save()
|
|
295
295
|
buff.seek(0)
|
|
296
|
-
result
|
|
296
|
+
result[page_num - 1] = buff.read()
|
|
297
297
|
|
|
298
298
|
return result
|
|
299
299
|
|
|
@@ -334,7 +334,7 @@ def merge_watermarks_with_pdf(
|
|
|
334
334
|
def _clone_page_widgets(
|
|
335
335
|
writer: PdfWriter,
|
|
336
336
|
page: PageObject,
|
|
337
|
-
keys: Optional[
|
|
337
|
+
keys: Optional[set[str]],
|
|
338
338
|
) -> List[Any]:
|
|
339
339
|
"""
|
|
340
340
|
Clones matching widgets from a single PDF page.
|
|
@@ -342,7 +342,7 @@ def _clone_page_widgets(
|
|
|
342
342
|
Args:
|
|
343
343
|
writer (PdfWriter): The PdfWriter for cloning.
|
|
344
344
|
page (PageObject): The source PDF page object.
|
|
345
|
-
keys (Optional[
|
|
345
|
+
keys (Optional[set[str]]): Keys of widgets to clone.
|
|
346
346
|
|
|
347
347
|
Returns:
|
|
348
348
|
List[Any]: A list of cloned widget objects.
|
|
@@ -358,7 +358,7 @@ def _clone_page_widgets(
|
|
|
358
358
|
def _collect_from_single_watermark_specific_page(
|
|
359
359
|
writer: PdfWriter,
|
|
360
360
|
watermark: bytes,
|
|
361
|
-
keys: Optional[
|
|
361
|
+
keys: Optional[set[str]],
|
|
362
362
|
page_num: int,
|
|
363
363
|
) -> Dict[int, List[Any]]:
|
|
364
364
|
"""
|
|
@@ -367,7 +367,7 @@ def _collect_from_single_watermark_specific_page(
|
|
|
367
367
|
Args:
|
|
368
368
|
writer (PdfWriter): The PdfWriter for cloning.
|
|
369
369
|
watermark (bytes): The watermark PDF byte stream.
|
|
370
|
-
keys (Optional[
|
|
370
|
+
keys (Optional[set[str]]): Keys of widgets to clone.
|
|
371
371
|
page_num (int): The page index within the watermark PDF.
|
|
372
372
|
|
|
373
373
|
Returns:
|
|
@@ -385,7 +385,7 @@ def _collect_from_single_watermark_specific_page(
|
|
|
385
385
|
def _collect_from_single_watermark_1_to_1(
|
|
386
386
|
writer: PdfWriter,
|
|
387
387
|
watermark: bytes,
|
|
388
|
-
keys: Optional[
|
|
388
|
+
keys: Optional[set[str]],
|
|
389
389
|
) -> Dict[int, List[Any]]:
|
|
390
390
|
"""
|
|
391
391
|
Maps pages 1:1 between a single watermark PDF and the output PDF.
|
|
@@ -393,7 +393,7 @@ def _collect_from_single_watermark_1_to_1(
|
|
|
393
393
|
Args:
|
|
394
394
|
writer (PdfWriter): The PdfWriter for cloning.
|
|
395
395
|
watermark (bytes): The watermark PDF byte stream.
|
|
396
|
-
keys (Optional[
|
|
396
|
+
keys (Optional[set[str]]): Keys of widgets to clone.
|
|
397
397
|
|
|
398
398
|
Returns:
|
|
399
399
|
Dict[int, List[Any]]: A dictionary mapping output page indices to cloned widgets.
|
|
@@ -408,7 +408,7 @@ def _collect_from_single_watermark_1_to_1(
|
|
|
408
408
|
def _collect_from_multiple_watermarks(
|
|
409
409
|
writer: PdfWriter,
|
|
410
410
|
watermarks: List[bytes],
|
|
411
|
-
keys: Optional[
|
|
411
|
+
keys: Optional[set[str]],
|
|
412
412
|
page_num: Optional[int],
|
|
413
413
|
) -> Dict[int, List[Any]]:
|
|
414
414
|
"""
|
|
@@ -417,7 +417,7 @@ def _collect_from_multiple_watermarks(
|
|
|
417
417
|
Args:
|
|
418
418
|
writer (PdfWriter): The PdfWriter for cloning.
|
|
419
419
|
watermarks (List[bytes]): A list of watermark PDF byte streams.
|
|
420
|
-
keys (Optional[
|
|
420
|
+
keys (Optional[set[str]]): Keys of widgets to clone.
|
|
421
421
|
page_num (Optional[int]): The page index within each watermark PDF.
|
|
422
422
|
|
|
423
423
|
Returns:
|
|
@@ -452,17 +452,19 @@ def _collect_widgets_to_copy(
|
|
|
452
452
|
Returns:
|
|
453
453
|
Dict[int, List[Any]]: A dictionary mapping output page indices to lists of cloned widgets.
|
|
454
454
|
"""
|
|
455
|
+
key_set = set(keys) if keys is not None else None
|
|
456
|
+
|
|
455
457
|
if isinstance(watermarks, bytes):
|
|
456
458
|
if page_num is not None:
|
|
457
459
|
# Case: Single watermark PDF, extracting a specific page to the first output page.
|
|
458
460
|
return _collect_from_single_watermark_specific_page(
|
|
459
|
-
writer, watermarks,
|
|
461
|
+
writer, watermarks, key_set, page_num
|
|
460
462
|
)
|
|
461
463
|
# Case: Single watermark PDF, mapping pages 1:1 to output pages.
|
|
462
|
-
return _collect_from_single_watermark_1_to_1(writer, watermarks,
|
|
464
|
+
return _collect_from_single_watermark_1_to_1(writer, watermarks, key_set)
|
|
463
465
|
|
|
464
466
|
# Case: List of watermark PDFs, each corresponding to an output page.
|
|
465
|
-
return _collect_from_multiple_watermarks(writer, watermarks,
|
|
467
|
+
return _collect_from_multiple_watermarks(writer, watermarks, key_set, page_num)
|
|
466
468
|
|
|
467
469
|
|
|
468
470
|
def _apply_widgets_to_pages(
|
|
@@ -104,7 +104,7 @@ class Widget:
|
|
|
104
104
|
self.hook_params.append((each, kwargs.get(each)))
|
|
105
105
|
|
|
106
106
|
@staticmethod
|
|
107
|
-
@lru_cache
|
|
107
|
+
@lru_cache(maxsize=128)
|
|
108
108
|
def _get_default_field_flags(acro_form_class: type, acro_form_func: str) -> tuple:
|
|
109
109
|
"""
|
|
110
110
|
Retrieves the default field flags for a ReportLab AcroForm method.
|
|
@@ -192,9 +192,9 @@ class Widget:
|
|
|
192
192
|
watermark for that page. Pages without any widgets will
|
|
193
193
|
have an empty byte string (b"").
|
|
194
194
|
"""
|
|
195
|
-
result = []
|
|
196
|
-
|
|
197
195
|
pdf = PdfReader(BytesIO(stream))
|
|
196
|
+
page_count = len(pdf.pages)
|
|
197
|
+
result = [b""] * page_count
|
|
198
198
|
|
|
199
199
|
widgets_by_page = {}
|
|
200
200
|
for widget in widgets:
|
|
@@ -202,15 +202,14 @@ class Widget:
|
|
|
202
202
|
widgets_by_page[widget.page_number] = []
|
|
203
203
|
widgets_by_page[widget.page_number].append(widget)
|
|
204
204
|
|
|
205
|
-
for
|
|
206
|
-
page_num = i + 1
|
|
205
|
+
for page_num in range(1, page_count + 1):
|
|
207
206
|
if page_num not in widgets_by_page:
|
|
208
|
-
result.append(b"")
|
|
209
207
|
continue
|
|
210
208
|
|
|
211
209
|
# Use a fresh buffer per page to avoid stale trailing bytes
|
|
212
210
|
# when the current page watermark is smaller than a previous page.
|
|
213
211
|
watermark = BytesIO()
|
|
212
|
+
page = pdf.pages[page_num - 1]
|
|
214
213
|
|
|
215
214
|
canvas = Canvas(
|
|
216
215
|
watermark,
|
|
@@ -227,7 +226,7 @@ class Widget:
|
|
|
227
226
|
canvas.showPage()
|
|
228
227
|
canvas.save()
|
|
229
228
|
watermark.seek(0)
|
|
230
|
-
result
|
|
229
|
+
result[page_num - 1] = watermark.read()
|
|
231
230
|
|
|
232
231
|
return result
|
|
233
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,14 +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
|
|
123
|
-
page_widgets = page_to_widgets.get(
|
|
122
|
+
for page_num in range(1, page_count + 1):
|
|
123
|
+
page_widgets = page_to_widgets.get(page_num, [])
|
|
124
124
|
if not page_widgets:
|
|
125
|
-
result.append(b"")
|
|
126
125
|
continue
|
|
127
126
|
|
|
128
127
|
# pylint: disable=R0801
|
|
129
128
|
watermark = BytesIO()
|
|
129
|
+
p = input_pdf.pages[page_num - 1]
|
|
130
130
|
canvas = Canvas(
|
|
131
131
|
watermark,
|
|
132
132
|
pagesize=(
|
|
@@ -169,7 +169,7 @@ class SignatureWidget:
|
|
|
169
169
|
with BytesIO() as f:
|
|
170
170
|
out.write(f)
|
|
171
171
|
f.seek(0)
|
|
172
|
-
result
|
|
172
|
+
result[page_num - 1] = f.read()
|
|
173
173
|
|
|
174
174
|
return result
|
|
175
175
|
|
|
@@ -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,11 +241,26 @@ 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
|
|
243
261
|
|
|
244
262
|
@staticmethod
|
|
245
|
-
@lru_cache
|
|
263
|
+
@lru_cache(maxsize=128)
|
|
246
264
|
def _get_page_streams_with_widgets(stream: bytes) -> tuple[bytes, ...]:
|
|
247
265
|
"""
|
|
248
266
|
Extracts page streams while preserving the original page widgets.
|
|
@@ -349,11 +367,14 @@ class PdfWrapper:
|
|
|
349
367
|
"""
|
|
350
368
|
Returns a list of the names of the currently registered fonts.
|
|
351
369
|
|
|
370
|
+
Accessing this property loads AcroForm fonts from the PDF stream if
|
|
371
|
+
they have not already been loaded.
|
|
372
|
+
|
|
352
373
|
Returns:
|
|
353
374
|
list: A list of font names (str).
|
|
354
375
|
"""
|
|
355
376
|
|
|
356
|
-
return list(self.
|
|
377
|
+
return list(self._ensure_available_fonts_loaded().keys())
|
|
357
378
|
|
|
358
379
|
@property
|
|
359
380
|
def pages(self) -> Sequence[PdfWrapper]:
|
|
@@ -453,15 +474,15 @@ class PdfWrapper:
|
|
|
453
474
|
"""
|
|
454
475
|
|
|
455
476
|
if any(widget.hooks_to_trigger for widget in self.widgets.values()):
|
|
477
|
+
available_fonts = self._ensure_available_fonts_loaded()
|
|
456
478
|
for widget in self.widgets.values():
|
|
457
479
|
if (
|
|
458
480
|
isinstance(widget, (Text, Dropdown))
|
|
459
|
-
and widget.font not in
|
|
460
|
-
and widget.font in
|
|
481
|
+
and widget.font not in available_fonts.values()
|
|
482
|
+
and widget.font in available_fonts
|
|
461
483
|
):
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
) # from `new_font` to `/F1`
|
|
484
|
+
# from `new_font` to `/F1`
|
|
485
|
+
widget.font = available_fonts.get(widget.font)
|
|
465
486
|
|
|
466
487
|
self._stream = trigger_widget_hooks(
|
|
467
488
|
self._stream,
|
|
@@ -796,7 +817,7 @@ class PdfWrapper:
|
|
|
796
817
|
Draws raw elements (text, images, etc.) directly onto the PDF pages.
|
|
797
818
|
|
|
798
819
|
This method is the primary mechanism for drawing non-form field content.
|
|
799
|
-
It takes a list of
|
|
820
|
+
It takes a list of raw element objects and renders them
|
|
800
821
|
onto the PDF document as watermarks.
|
|
801
822
|
|
|
802
823
|
Args:
|
|
@@ -844,6 +865,7 @@ class PdfWrapper:
|
|
|
844
865
|
ttf_file = fp_or_f_obj_or_stream_to_stream(ttf_file)
|
|
845
866
|
|
|
846
867
|
if validate_font(font_name, ttf_file) if ttf_file is not None else False:
|
|
868
|
+
self._ensure_available_fonts_loaded()
|
|
847
869
|
self._stream, new_font_name = register_font_acroform(
|
|
848
870
|
self._read(), ttf_file, getattr(self, "need_appearances")
|
|
849
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
|
|
@@ -29,7 +29,7 @@ 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>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|