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.
Files changed (86) hide show
  1. {pypdfform-5.2.1 → pypdfform-5.2.2}/PKG-INFO +9 -3
  2. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/egress.py +5 -2
  4. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/font.py +19 -5
  5. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/image.py +3 -5
  6. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/patterns.py +2 -1
  7. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/text.py +2 -2
  8. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/template.py +31 -9
  9. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/types.py +1 -1
  10. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/utils.py +1 -1
  11. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/watermark.py +24 -22
  12. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/base.py +6 -7
  13. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/signature.py +6 -6
  14. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/wrapper.py +37 -15
  15. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/PKG-INFO +9 -3
  16. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/requires.txt +1 -1
  17. {pypdfform-5.2.1 → pypdfform-5.2.2}/README.md +7 -1
  18. {pypdfform-5.2.1 → pypdfform-5.2.2}/pyproject.toml +1 -1
  19. {pypdfform-5.2.1 → pypdfform-5.2.2}/LICENSE +0 -0
  20. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/__init__.py +0 -0
  21. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/common.py +0 -0
  22. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/create.py +0 -0
  23. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/entry.py +0 -0
  24. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/inspect.py +0 -0
  25. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/remove.py +0 -0
  26. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/root.py +0 -0
  27. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/__init__.py +0 -0
  28. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/create.py +0 -0
  29. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/update.py +0 -0
  30. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/cli/update.py +0 -0
  31. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/__init__.py +0 -0
  32. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/adapter.py +0 -0
  33. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/__init__.py +0 -0
  34. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/base.py +0 -0
  35. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/link.py +0 -0
  36. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/stamp.py +0 -0
  37. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/text.py +0 -0
  38. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  39. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/assets/__init__.py +0 -0
  40. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/assets/bedrock.py +0 -0
  41. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/assets/blank.py +0 -0
  42. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/constants.py +0 -0
  43. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/coordinate.py +0 -0
  44. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/deprecation.py +0 -0
  45. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/filler.py +0 -0
  46. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/hooks.py +0 -0
  47. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/__init__.py +0 -0
  48. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/base.py +0 -0
  49. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  50. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  51. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/image.py +0 -0
  52. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/radio.py +0 -0
  53. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/signature.py +0 -0
  54. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/text.py +0 -0
  55. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/__init__.py +0 -0
  56. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/circle.py +0 -0
  57. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/ellipse.py +0 -0
  58. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/image.py +0 -0
  59. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/line.py +0 -0
  60. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/raw/rect.py +0 -0
  61. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/__init__.py +0 -0
  62. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  63. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  64. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/image.py +0 -0
  65. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/radio.py +0 -0
  66. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/text.py +0 -0
  67. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  68. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  69. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/entry_points.txt +0 -0
  70. {pypdfform-5.2.1 → pypdfform-5.2.2}/PyPDFForm.egg-info/top_level.txt +0 -0
  71. {pypdfform-5.2.1 → pypdfform-5.2.2}/setup.cfg +0 -0
  72. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_bulk_create_fields.py +0 -0
  73. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_create_widget.py +0 -0
  74. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_draw_elements.py +0 -0
  75. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_dropdown.py +0 -0
  76. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_extract_middleware_attributes.py +0 -0
  77. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_fill_max_length_text_field.py +0 -0
  78. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_font_widths.py +0 -0
  79. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_functional.py +0 -0
  80. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_generate_appearance_streams.py +0 -0
  81. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_js.py +0 -0
  82. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_need_appearances.py +0 -0
  83. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_paragraph.py +0 -0
  84. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_signature.py +0 -0
  85. {pypdfform-5.2.1 → pypdfform-5.2.2}/tests/test_use_full_widget_name.py +0 -0
  86. {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.1
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<5.0.0,>=4.5.1
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
- [![Stargazers over time](https://starchart.cc/chinapandaman/PyPDFForm.svg?variant=adaptive)](https://starchart.cc/chinapandaman/PyPDFForm)
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>
@@ -18,7 +18,7 @@ from Python code or from the command line.
18
18
 
19
19
  import logging
20
20
 
21
- __version__ = "5.2.1"
21
+ __version__ = "5.2.2"
22
22
 
23
23
  from .lib.annotations import Annotations
24
24
  from .lib.assets.blank import BlankPage
@@ -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
- with Pdf.open(BytesIO(result)) as f:
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 = compress(ttf_stream)
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 annotation
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-255 for each channel).
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 a dictionary of widgets from a PDF stream.
68
+ Builds an independent dictionary of widgets from a PDF stream.
68
69
 
69
- This function parses a PDF stream to identify and construct widgets
70
- present in the PDF form. It iterates through each page and its annotations,
71
- extracting widget properties such as key, description, max length (for text fields),
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 keys:
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 an empty list.
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 ("image", "text", or "line")
248
- along with type-specific parameters.
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 i, page in enumerate(pdf_file.pages):
274
- elements = page_to_to_draw[i + 1]
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.append(buff.read())
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[List[str]],
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[List[str]]): Keys of widgets to clone.
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[List[str]],
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[List[str]]): Keys of widgets to clone.
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[List[str]],
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[List[str]]): Keys of widgets to clone.
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[List[str]],
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[List[str]]): Keys of widgets to clone.
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, keys, page_num
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, keys)
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, keys, page_num)
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 i, page in enumerate(pdf.pages):
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.append(watermark.read())
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 i, p in enumerate(input_pdf.pages):
123
- page_widgets = page_to_widgets.get(i + 1, [])
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.append(f.read())
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""), which creates a blank PDF.
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 and available fonts.
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 updates the available fonts.
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
- self._read(),
223
+ stream,
221
224
  getattr(self, "use_full_widget_name"),
222
225
  )
223
- if self._read()
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
- if self._read():
242
- self._available_fonts.update(**get_all_available_fonts(self._read()))
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._available_fonts.keys())
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 self._available_fonts.values()
460
- and widget.font in self._available_fonts
481
+ and widget.font not in available_fonts.values()
482
+ and widget.font in available_fonts
461
483
  ):
462
- widget.font = self._available_fonts.get(
463
- widget.font
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 `RawText` or `RawImage` objects and renders them
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.1
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<5.0.0,>=4.5.1
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
- [![Stargazers over time](https://starchart.cc/chinapandaman/PyPDFForm.svg?variant=adaptive)](https://starchart.cc/chinapandaman/PyPDFForm)
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>
@@ -3,7 +3,7 @@ fonttools<5.0.0,>=4.63.0
3
3
  pikepdf<11.0.0,>=10.7.2
4
4
  pillow<13.0.0,>=12.2.0
5
5
  pypdf<7.0.0,>=6.12.2
6
- reportlab<5.0.0,>=4.5.1
6
+ reportlab<6.0.0,>=4.5.1
7
7
 
8
8
  [cli]
9
9
  typer<1.0.0,>=0.26.1
@@ -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
- [![Stargazers over time](https://starchart.cc/chinapandaman/PyPDFForm.svg?variant=adaptive)](https://starchart.cc/chinapandaman/PyPDFForm)
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>
@@ -34,7 +34,7 @@ dependencies = [
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,<5.0.0",
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