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.
Files changed (86) hide show
  1. {pypdfform-5.2.0 → pypdfform-5.2.2}/PKG-INFO +10 -4
  2. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/egress.py +5 -2
  4. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/filler.py +3 -1
  5. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/font.py +19 -5
  6. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/image.py +8 -13
  7. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/patterns.py +2 -1
  8. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/text.py +2 -2
  9. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/template.py +56 -26
  10. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/types.py +1 -1
  11. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/utils.py +1 -1
  12. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/watermark.py +40 -29
  13. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/base.py +30 -13
  14. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/signature.py +9 -6
  15. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/wrapper.py +61 -19
  16. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/PKG-INFO +10 -4
  17. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/requires.txt +2 -2
  18. {pypdfform-5.2.0 → pypdfform-5.2.2}/README.md +7 -1
  19. {pypdfform-5.2.0 → pypdfform-5.2.2}/pyproject.toml +2 -2
  20. {pypdfform-5.2.0 → pypdfform-5.2.2}/LICENSE +0 -0
  21. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/__init__.py +0 -0
  22. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/common.py +0 -0
  23. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/create.py +0 -0
  24. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/entry.py +0 -0
  25. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/inspect.py +0 -0
  26. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/remove.py +0 -0
  27. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/root.py +0 -0
  28. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/__init__.py +0 -0
  29. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/create.py +0 -0
  30. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/schemas/update.py +0 -0
  31. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/cli/update.py +0 -0
  32. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/__init__.py +0 -0
  33. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/adapter.py +0 -0
  34. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/__init__.py +0 -0
  35. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/base.py +0 -0
  36. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/link.py +0 -0
  37. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/stamp.py +0 -0
  38. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/text.py +0 -0
  39. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  40. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/assets/__init__.py +0 -0
  41. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/assets/bedrock.py +0 -0
  42. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/assets/blank.py +0 -0
  43. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/constants.py +0 -0
  44. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/coordinate.py +0 -0
  45. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/deprecation.py +0 -0
  46. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/hooks.py +0 -0
  47. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/__init__.py +0 -0
  48. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/base.py +0 -0
  49. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  50. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  51. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/image.py +0 -0
  52. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/radio.py +0 -0
  53. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/signature.py +0 -0
  54. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/middleware/text.py +0 -0
  55. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/__init__.py +0 -0
  56. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/circle.py +0 -0
  57. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/ellipse.py +0 -0
  58. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/image.py +0 -0
  59. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/line.py +0 -0
  60. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/raw/rect.py +0 -0
  61. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/__init__.py +0 -0
  62. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  63. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  64. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/image.py +0 -0
  65. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/radio.py +0 -0
  66. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm/lib/widgets/text.py +0 -0
  67. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  68. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  69. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/entry_points.txt +0 -0
  70. {pypdfform-5.2.0 → pypdfform-5.2.2}/PyPDFForm.egg-info/top_level.txt +0 -0
  71. {pypdfform-5.2.0 → pypdfform-5.2.2}/setup.cfg +0 -0
  72. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_bulk_create_fields.py +0 -0
  73. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_create_widget.py +0 -0
  74. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_draw_elements.py +0 -0
  75. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_dropdown.py +0 -0
  76. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_extract_middleware_attributes.py +0 -0
  77. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_fill_max_length_text_field.py +0 -0
  78. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_font_widths.py +0 -0
  79. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_functional.py +0 -0
  80. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_generate_appearance_streams.py +0 -0
  81. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_js.py +0 -0
  82. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_need_appearances.py +0 -0
  83. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_paragraph.py +0 -0
  84. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_signature.py +0 -0
  85. {pypdfform-5.2.0 → pypdfform-5.2.2}/tests/test_use_full_widget_name.py +0 -0
  86. {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.0
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<49.0.0,>=48.0.0
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<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.0"
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)
@@ -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 = get_image_dimensions(stream)
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 = 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.
@@ -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 byte stream
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
- buff = BytesIO()
67
- buff.write(image_stream)
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 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
@@ -449,8 +471,7 @@ def update_widget_keys(
449
471
  out = PdfWriter()
450
472
  out.append(pdf)
451
473
 
452
- for i, old_key in enumerate(old_keys):
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 _update_single_widget_key(
482
+ def _apply_widget_key_updates(
462
483
  writer: PdfWriter,
463
484
  widgets: Dict[str, WIDGET_TYPES],
464
- old_key: str,
465
- new_key: str,
466
- index: int,
485
+ old_keys: List[str],
486
+ new_keys: List[str],
487
+ indices: List[int],
467
488
  ) -> None:
468
489
  """
469
- Updates a single widget key in a PDF template.
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
- old_key (str): The old widget key to be replaced.
475
- new_key (str): The new widget key to replace the old key.
476
- index (int): The index of the widget to update if multiple widgets have the same name.
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
- tracker = -1
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
- widget = widgets.get(key)
485
- if widget is None or old_key != key:
512
+ if key not in updates:
486
513
  continue
487
514
 
488
- tracker += 1
489
- if not isinstance(widget, Radio) and tracker != index:
490
- continue
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
- update_annotation_name(annot, new_key)
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 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.
@@ -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
- ImageReader(image_buff),
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 ("image", "text", or "line")
239
- 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.
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 i, page in enumerate(pdf_file.pages):
265
- 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, [])
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.append(buff.read())
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[List[str]],
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[List[str]]): Keys of widgets to clone.
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[List[str]],
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[List[str]]): Keys of widgets to clone.
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[List[str]],
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[List[str]]): Keys of widgets to clone.
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[List[str]],
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[List[str]]): Keys of widgets to clone.
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, keys, page_num
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, keys)
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, keys, page_num)
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 = signature(
119
- getattr(canvas.acroForm, self.ACRO_FORM_FUNC)
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 i, page in enumerate(pdf.pages):
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.append(watermark.read())
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 i, p in enumerate(input_pdf.pages):
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.append(f.read())
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 cached_property
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""), 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,8 +241,44 @@ 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
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._available_fonts.keys())
377
+ return list(self._ensure_available_fonts_loaded().keys())
336
378
 
337
- @cached_property
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
- # Case: Single watermark PDF, extracting a specific page to the first output page.
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 i, each in enumerate(get_page_streams(remove_all_widgets(self._read())))
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 self._available_fonts.values()
440
- and widget.font in self._available_fonts
481
+ and widget.font not in available_fonts.values()
482
+ and widget.font in available_fonts
441
483
  ):
442
- widget.font = self._available_fonts.get(
443
- widget.font
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 `RawText` or `RawImage` objects and renders them
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.0
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<49.0.0,>=48.0.0
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<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>
@@ -1,9 +1,9 @@
1
- cryptography<49.0.0,>=48.0.0
1
+ cryptography<50.0.0,>=48.0.0
2
2
  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>
@@ -29,12 +29,12 @@ classifiers = [
29
29
  ]
30
30
  requires-python = ">=3.10"
31
31
  dependencies = [
32
- "cryptography>=48.0.0,<49.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,<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