PyPDFForm 4.7.3__tar.gz → 4.7.5__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 (73) hide show
  1. {pypdfform-4.7.3 → pypdfform-4.7.5}/PKG-INFO +2 -2
  2. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/coordinate.py +2 -2
  4. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/egress.py +3 -4
  5. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/filler.py +1 -2
  6. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/font.py +77 -26
  7. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/hooks.py +1 -2
  8. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/template.py +5 -5
  9. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/utils.py +6 -28
  10. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/watermark.py +17 -34
  11. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/widgets/base.py +1 -2
  12. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/widgets/signature.py +7 -7
  13. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/wrapper.py +8 -7
  14. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm.egg-info/PKG-INFO +2 -2
  15. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm.egg-info/requires.txt +1 -1
  16. {pypdfform-4.7.3 → pypdfform-4.7.5}/pyproject.toml +2 -2
  17. {pypdfform-4.7.3 → pypdfform-4.7.5}/LICENSE +0 -0
  18. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/adapter.py +0 -0
  19. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/annotations/__init__.py +0 -0
  20. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/annotations/base.py +0 -0
  21. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/annotations/link.py +0 -0
  22. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/annotations/stamp.py +0 -0
  23. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/annotations/text.py +0 -0
  24. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/annotations/text_markup.py +0 -0
  25. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/assets/__init__.py +0 -0
  26. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/assets/bedrock.py +0 -0
  27. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/assets/blank.py +0 -0
  28. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/constants.py +0 -0
  29. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/deprecation.py +0 -0
  30. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/image.py +0 -0
  31. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/middleware/__init__.py +0 -0
  32. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/middleware/base.py +0 -0
  33. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/middleware/checkbox.py +0 -0
  34. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/middleware/dropdown.py +0 -0
  35. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/middleware/image.py +0 -0
  36. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/middleware/radio.py +0 -0
  37. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/middleware/signature.py +0 -0
  38. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/middleware/text.py +0 -0
  39. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/patterns.py +0 -0
  40. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/raw/__init__.py +0 -0
  41. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/raw/circle.py +0 -0
  42. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/raw/ellipse.py +0 -0
  43. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/raw/image.py +0 -0
  44. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/raw/line.py +0 -0
  45. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/raw/rect.py +0 -0
  46. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/raw/text.py +0 -0
  47. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/types.py +0 -0
  48. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/widgets/__init__.py +0 -0
  49. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/widgets/checkbox.py +0 -0
  50. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/widgets/dropdown.py +0 -0
  51. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/widgets/image.py +0 -0
  52. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/widgets/radio.py +0 -0
  53. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm/widgets/text.py +0 -0
  54. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  55. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  56. {pypdfform-4.7.3 → pypdfform-4.7.5}/PyPDFForm.egg-info/top_level.txt +0 -0
  57. {pypdfform-4.7.3 → pypdfform-4.7.5}/README.md +0 -0
  58. {pypdfform-4.7.3 → pypdfform-4.7.5}/setup.cfg +0 -0
  59. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_bulk_create_fields.py +0 -0
  60. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_create_widget.py +0 -0
  61. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_draw_elements.py +0 -0
  62. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_dropdown.py +0 -0
  63. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_extract_middleware_attributes.py +0 -0
  64. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_fill_max_length_text_field.py +0 -0
  65. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_font_widths.py +0 -0
  66. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_functional.py +0 -0
  67. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_generate_appearance_streams.py +0 -0
  68. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_js.py +0 -0
  69. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_need_appearances.py +0 -0
  70. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_paragraph.py +0 -0
  71. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_signature.py +0 -0
  72. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_use_full_widget_name.py +0 -0
  73. {pypdfform-4.7.3 → pypdfform-4.7.5}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.7.3
3
+ Version: 4.7.5
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -24,7 +24,7 @@ Requires-Dist: cryptography<47.0.0,>=46.0.3
24
24
  Requires-Dist: fonttools<5.0.0,>=4.60.1
25
25
  Requires-Dist: pikepdf<11.0.0,>=10.5.0
26
26
  Requires-Dist: pillow<13.0.0,>=12.0.0
27
- Requires-Dist: pypdf<7.0.0,>=6.3.0
27
+ Requires-Dist: pypdf<7.0.0,>=6.9.0
28
28
  Requires-Dist: reportlab<5.0.0,>=4.4.6
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: black<27.0.0,>=25.11.0; extra == "dev"
@@ -22,7 +22,7 @@ PyPDFForm aims to simplify PDF form manipulation, making it accessible to develo
22
22
 
23
23
  import logging
24
24
 
25
- __version__ = "4.7.3"
25
+ __version__ = "4.7.5"
26
26
 
27
27
  from .annotations import Annotations
28
28
  from .assets.blank import BlankPage
@@ -7,6 +7,7 @@ It allows developers to visualize the coordinate system of each page in a PDF, w
7
7
  for debugging and precisely positioning elements when filling or drawing on PDF forms.
8
8
  """
9
9
 
10
+ from io import BytesIO
10
11
  from typing import Tuple
11
12
 
12
13
  from pypdf import PdfReader
@@ -14,7 +15,6 @@ from reportlab.pdfbase.pdfmetrics import stringWidth
14
15
 
15
16
  from .constants import COORDINATE_GRID_FONT_SIZE_MARGIN_RATIO, DEFAULT_FONT
16
17
  from .middleware.text import Text
17
- from .utils import stream_to_io
18
18
  from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
19
19
 
20
20
 
@@ -39,7 +39,7 @@ def generate_coordinate_grid(
39
39
  Returns:
40
40
  bytes: The PDF file with the coordinate grid overlay as bytes.
41
41
  """
42
- pdf_file = PdfReader(stream_to_io(pdf))
42
+ pdf_file = PdfReader(BytesIO(pdf))
43
43
  lines_by_page = {}
44
44
  texts_by_page = {}
45
45
 
@@ -19,7 +19,6 @@ from pypdf.generic import DictionaryObject, NameObject, TextStringObject
19
19
 
20
20
  from .constants import (JS, XFA, AcroForm, JavaScript, OpenAction, Root, S,
21
21
  Title)
22
- from .utils import stream_to_io
23
22
 
24
23
 
25
24
  @lru_cache
@@ -44,7 +43,7 @@ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) ->
44
43
  Returns:
45
44
  bytes: The modified PDF content as a bytes stream.
46
45
  """
47
- reader = PdfReader(stream_to_io(pdf))
46
+ reader = PdfReader(BytesIO(pdf))
48
47
  writer = PdfWriter()
49
48
 
50
49
  if AcroForm in reader.trailer[Root] and XFA in reader.trailer[Root][AcroForm]:
@@ -59,7 +58,7 @@ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) ->
59
58
  result = f.read()
60
59
 
61
60
  if generate_appearance_streams:
62
- with Pdf.open(stream_to_io(result)) as f:
61
+ with Pdf.open(BytesIO(result)) as f:
63
62
  f.generate_appearance_streams()
64
63
  with BytesIO() as r:
65
64
  f.save(r)
@@ -87,7 +86,7 @@ def preserve_pdf_properties(
87
86
  Returns:
88
87
  bytes: The modified PDF content as a bytes stream.
89
88
  """
90
- reader = PdfReader(stream_to_io(pdf))
89
+ reader = PdfReader(BytesIO(pdf))
91
90
  writer = PdfWriter()
92
91
  writer.append(reader)
93
92
 
@@ -27,7 +27,6 @@ from .middleware.text import Text
27
27
  from .patterns import (get_widget_key, update_checkbox_value,
28
28
  update_dropdown_value, update_radio_value,
29
29
  update_text_value)
30
- from .utils import stream_to_io
31
30
  from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
32
31
 
33
32
 
@@ -177,7 +176,7 @@ def fill(
177
176
  The image drawn stream is only returned if there are any image or signature widgets
178
177
  in the form.
179
178
  """
180
- pdf = PdfReader(stream_to_io(template))
179
+ pdf = PdfReader(BytesIO(template))
181
180
  out = PdfWriter()
182
181
  out.append(pdf)
183
182
 
@@ -7,17 +7,21 @@ allowing these fonts to be used when filling form fields. The module also provid
7
7
  for extracting font information from TTF streams and managing font names within a PDF.
8
8
  """
9
9
 
10
+ from contextlib import contextmanager
10
11
  from functools import lru_cache
11
12
  from io import BytesIO
13
+ from typing import Generator
14
+ from uuid import uuid4
12
15
  from zlib import compress
13
16
 
14
17
  from fontTools.ttLib import TTFont as FT_TTFont
15
18
  from pypdf import PdfReader, PdfWriter
16
19
  from pypdf.generic import (ArrayObject, DictionaryObject, FloatObject,
17
20
  NameObject, NumberObject, StreamObject)
18
- from reportlab.pdfbase.pdfmetrics import registerFont
21
+ from reportlab.pdfbase.pdfmetrics import _fonts
19
22
  from reportlab.pdfbase.ttfonts import TTFError, TTFont
20
23
 
24
+ from .assets.blank import BlankPage
21
25
  from .constants import (DEFAULT_ASSUMED_GLYPH_WIDTH, DR, EM_TO_PDF_FACTOR,
22
26
  ENCODING_TABLE_SIZE, FIRST_CHAR_CODE, FONT_NAME_PREFIX,
23
27
  LAST_CHAR_CODE, AcroForm, BaseFont, Encoding, Fields,
@@ -26,26 +30,26 @@ from .constants import (DEFAULT_ASSUMED_GLYPH_WIDTH, DR, EM_TO_PDF_FACTOR,
26
30
  FontName, FontNotdef, LastChar, Length, Length1,
27
31
  MissingWidth, Resources, Subtype, TrueType, Type,
28
32
  Widths, WinAnsiEncoding)
29
- from .utils import stream_to_io
30
- from .watermark import get_watermark_with_font
33
+ from .raw.text import RawText
34
+ from .watermark import create_watermarks_and_draw
31
35
 
32
36
 
33
37
  @lru_cache
34
- def register_font(font_name: str, ttf_stream: bytes) -> bool:
38
+ def validate_font(font_name: str, ttf_stream: bytes) -> bool:
35
39
  """
36
- Registers a TrueType font with the ReportLab library.
40
+ Validates a TrueType font stream.
37
41
 
38
- This allows the font to be used for generating PDF documents with ReportLab.
42
+ This checks if the provided stream is a valid TrueType font by parsing it
43
+ with ReportLab's TTFont.
39
44
 
40
45
  Args:
41
- font_name (str): The name to register the font under. This name will be used
42
- to reference the font when creating PDF documents with ReportLab.
46
+ font_name (str): The name of the font.
43
47
  ttf_stream (bytes): The font file data in TTF format. This should be the raw
44
48
  bytes of the TTF file.
45
49
 
46
50
  Returns:
47
- bool: True if the font was registered successfully, False otherwise.
48
- Returns False if a TTFError occurs during registration, which usually
51
+ bool: True if the font stream is valid, False otherwise.
52
+ Returns False if a TTFError occurs during parsing, which usually
49
53
  indicates an invalid TTF stream.
50
54
  """
51
55
  buff = BytesIO()
@@ -53,7 +57,7 @@ def register_font(font_name: str, ttf_stream: bytes) -> bool:
53
57
  buff.seek(0)
54
58
 
55
59
  try:
56
- registerFont(TTFont(name=font_name, filename=buff))
60
+ TTFont(name=font_name, filename=buff)
57
61
  result = True
58
62
  except TTFError:
59
63
  result = False
@@ -62,7 +66,7 @@ def register_font(font_name: str, ttf_stream: bytes) -> bool:
62
66
  return result
63
67
 
64
68
 
65
- def get_additional_font_params(pdf: bytes, base_font_name: str) -> tuple:
69
+ def _get_additional_font_params(pdf: bytes, base_font_name: str) -> tuple:
66
70
  """
67
71
  Retrieves additional font parameters from a PDF document for a given base font name.
68
72
 
@@ -83,7 +87,7 @@ def get_additional_font_params(pdf: bytes, base_font_name: str) -> tuple:
83
87
  """
84
88
  font_descriptor_params = {}
85
89
  font_dict_params = {}
86
- reader = PdfReader(stream_to_io(pdf))
90
+ reader = PdfReader(BytesIO(pdf))
87
91
  first_page = reader.get_page(0)
88
92
 
89
93
  for font in first_page[Resources][Font].values():
@@ -144,8 +148,58 @@ def compute_font_glyph_widths(ttf_file: BytesIO, missing_width: float) -> list[f
144
148
  return widths
145
149
 
146
150
 
151
+ @contextmanager
152
+ def temporary_font_registration(
153
+ fonts: list[tuple[str, bytes]],
154
+ ) -> Generator[dict[str, str], None, None]:
155
+ """
156
+ Registers a list of fonts temporarily with unique names, yielding a mapping
157
+ from the original font names to the unique names.
158
+
159
+ Args:
160
+ fonts (list[tuple[str, bytes]]): A list of tuples, each containing a font name and its TTF stream.
161
+
162
+ Yields:
163
+ dict: A mapping of the original font names to the temporary unique names used by ReportLab.
164
+ """
165
+ font_mapping = {}
166
+ for font_name, ttf_stream in fonts:
167
+ rl_name = uuid4().hex
168
+ font_mapping[font_name] = rl_name
169
+ _fonts[rl_name] = TTFont(rl_name, BytesIO(ttf_stream))
170
+
171
+ try:
172
+ yield font_mapping
173
+ finally:
174
+ for rl_name in font_mapping.values():
175
+ if rl_name in _fonts:
176
+ del _fonts[rl_name]
177
+
178
+
179
+ @lru_cache
180
+ def _get_watermark_with_font(ttf_stream: bytes) -> bytes:
181
+ """
182
+ Creates a watermark PDF with a single space character using the specified font.
183
+
184
+ This function is primarily used to generate a dummy PDF page that includes
185
+ a specific font, which can then be merged with another PDF to ensure the
186
+ font is available or embedded. The result is cached for performance.
187
+
188
+ Args:
189
+ ttf_stream (bytes): The TrueType font stream to use.
190
+
191
+ Returns:
192
+ bytes: The watermark PDF as a byte stream.
193
+ """
194
+ with temporary_font_registration([("temp", ttf_stream)]) as font_mapping:
195
+ return create_watermarks_and_draw(
196
+ BlankPage().read(),
197
+ [RawText(" ", 1, 0, 0, font=font_mapping["temp"]).to_draw],
198
+ )[0]
199
+
200
+
147
201
  def register_font_acroform(
148
- pdf: bytes, font_name: str, ttf_stream: bytes, need_appearances: bool
202
+ pdf: bytes, ttf_stream: bytes, need_appearances: bool
149
203
  ) -> tuple:
150
204
  """
151
205
  Registers a TrueType font within the PDF's AcroForm dictionary.
@@ -157,7 +211,6 @@ def register_font_acroform(
157
211
  Args:
158
212
  pdf (bytes): The PDF file data as bytes. This is the PDF document that
159
213
  will be modified to include the new font.
160
- font_name (str): The name of the font being registered.
161
214
  ttf_stream (bytes): The font file data in TTF format as bytes. This is the
162
215
  raw data of the TrueType font file.
163
216
  need_appearances (bool): If True, attempts to retrieve existing font parameters
@@ -168,16 +221,16 @@ def register_font_acroform(
168
221
  tuple: A tuple containing the modified PDF data as bytes and the new font name
169
222
  (str) that was assigned to the registered font within the PDF.
170
223
  """
171
- base_font_name = get_base_font_name(ttf_stream)
172
- reader = PdfReader(stream_to_io(pdf))
224
+ base_font_name = _get_base_font_name(ttf_stream)
225
+ reader = PdfReader(BytesIO(pdf))
173
226
  writer = PdfWriter()
174
227
  writer.append(reader)
175
228
 
176
229
  font_descriptor_params = {}
177
230
  font_dict_params = {}
178
231
  if need_appearances:
179
- font_descriptor_params, font_dict_params = get_additional_font_params(
180
- get_watermark_with_font(font_name), base_font_name
232
+ font_descriptor_params, font_dict_params = _get_additional_font_params(
233
+ _get_watermark_with_font(ttf_stream), base_font_name
181
234
  )
182
235
 
183
236
  font_file_stream = StreamObject()
@@ -245,7 +298,7 @@ def register_font_acroform(
245
298
  dr[NameObject(Font)] = DictionaryObject()
246
299
  fonts = dr[Font]
247
300
 
248
- new_font_name = get_new_font_name(fonts)
301
+ new_font_name = _get_new_font_name(fonts)
249
302
  fonts[NameObject(new_font_name)] = font_dict_ref
250
303
 
251
304
  with BytesIO() as f:
@@ -255,7 +308,7 @@ def register_font_acroform(
255
308
 
256
309
 
257
310
  @lru_cache
258
- def get_base_font_name(ttf_stream: bytes) -> str:
311
+ def _get_base_font_name(ttf_stream: bytes) -> str:
259
312
  """
260
313
  Extracts the base font name from a TrueType font stream.
261
314
 
@@ -269,12 +322,10 @@ def get_base_font_name(ttf_stream: bytes) -> str:
269
322
  Returns:
270
323
  str: The base font name, prefixed with a forward slash.
271
324
  """
272
- return (
273
- f"/{TTFont(name='new_font', filename=stream_to_io(ttf_stream)).face.name.ustr}"
274
- )
325
+ return f"/{TTFont(name='new_font', filename=BytesIO(ttf_stream)).face.name.ustr}"
275
326
 
276
327
 
277
- def get_new_font_name(fonts: dict) -> str:
328
+ def _get_new_font_name(fonts: dict) -> str:
278
329
  """
279
330
  Generates a new unique font name to avoid conflicts with existing fonts in the PDF.
280
331
 
@@ -314,7 +365,7 @@ def get_all_available_fonts(pdf: bytes) -> dict:
314
365
  (without the leading slash) and the values are the corresponding font
315
366
  identifiers in the PDF. Returns an empty dictionary if no fonts are found.
316
367
  """
317
- reader = PdfReader(stream_to_io(pdf))
368
+ reader = PdfReader(BytesIO(pdf))
318
369
  try:
319
370
  fonts = reader.root_object[AcroForm][DR][Font]
320
371
  except KeyError:
@@ -24,7 +24,6 @@ from .constants import (AA, COMB, DA, FONT_COLOR_IDENTIFIER,
24
24
  JavaScript, MaxLen, Opt, Parent, Q, Rect, S, Type, U,
25
25
  X)
26
26
  from .patterns import get_widget_key
27
- from .utils import stream_to_io
28
27
 
29
28
 
30
29
  def trigger_widget_hooks(
@@ -52,7 +51,7 @@ def trigger_widget_hooks(
52
51
  Returns:
53
52
  bytes: The modified PDF data as bytes, with the widget hooks applied.
54
53
  """
55
- pdf_file = PdfReader(stream_to_io(pdf))
54
+ pdf_file = PdfReader(BytesIO(pdf))
56
55
  output = PdfWriter()
57
56
  output.append(pdf_file)
58
57
 
@@ -28,7 +28,7 @@ from .patterns import (WIDGET_DESCRIPTION_PATTERNS, WIDGET_TYPE_PATTERNS,
28
28
  get_field_hidden, get_field_rect, get_radio_value,
29
29
  get_text_field_alignment, get_text_field_max_length,
30
30
  get_text_value, get_widget_key, update_annotation_name)
31
- from .utils import extract_widget_property, find_pattern_match, stream_to_io
31
+ from .utils import extract_widget_property, find_pattern_match
32
32
 
33
33
 
34
34
  @lru_cache
@@ -45,7 +45,7 @@ def get_metadata(pdf: bytes) -> dict:
45
45
  if not pdf:
46
46
  return {}
47
47
 
48
- reader = PdfReader(stream_to_io(pdf))
48
+ reader = PdfReader(BytesIO(pdf))
49
49
  return reader.metadata or {}
50
50
 
51
51
 
@@ -218,7 +218,7 @@ def get_widgets_by_page(pdf: bytes) -> Dict[int, List[dict]]:
218
218
  Dict[int, List[dict]]: A dictionary where keys are page numbers (1-indexed)
219
219
  and values are lists of widget dictionaries.
220
220
  """
221
- pdf_file = PdfReader(stream_to_io(pdf))
221
+ pdf_file = PdfReader(BytesIO(pdf))
222
222
 
223
223
  result = {}
224
224
 
@@ -335,7 +335,7 @@ def create_annotations(
335
335
  Returns:
336
336
  bytes: The updated PDF stream with the added annotations.
337
337
  """
338
- writer = PdfWriter(stream_to_io(template))
338
+ writer = PdfWriter(BytesIO(template))
339
339
  annotations_by_page = _group_annotations_by_page(annotations)
340
340
 
341
341
  for i, page in enumerate(writer.pages):
@@ -382,7 +382,7 @@ def update_widget_keys(
382
382
  Returns:
383
383
  bytes: The updated PDF template as a byte stream.
384
384
  """
385
- pdf = PdfReader(stream_to_io(template))
385
+ pdf = PdfReader(BytesIO(template))
386
386
  out = PdfWriter()
387
387
  out.append(pdf)
388
388
 
@@ -18,7 +18,7 @@ from functools import lru_cache
18
18
  from io import BytesIO
19
19
  from secrets import choice
20
20
  from string import ascii_letters, digits, punctuation
21
- from typing import Any, BinaryIO, List
21
+ from typing import Any, List
22
22
 
23
23
  from pypdf import PdfReader, PdfWriter
24
24
  from pypdf.generic import ArrayObject, DictionaryObject, NameObject
@@ -26,28 +26,6 @@ from pypdf.generic import ArrayObject, DictionaryObject, NameObject
26
26
  from .constants import SLASH, UNIQUE_SUFFIX_LENGTH, Annots
27
27
 
28
28
 
29
- def stream_to_io(stream: bytes) -> BinaryIO:
30
- """
31
- Converts a bytes stream to a BinaryIO object, which can be used by PyPDFForm.
32
-
33
- This function takes a bytes stream as input and returns a BinaryIO object
34
- that represents the same data. This is useful because PyPDFForm often
35
- works with BinaryIO objects, so this function allows you to easily convert
36
- a bytes stream to the correct format.
37
-
38
- Args:
39
- stream (bytes): The bytes stream to convert.
40
-
41
- Returns:
42
- BinaryIO: A BinaryIO object representing the stream.
43
- """
44
- result = BytesIO()
45
- result.write(stream)
46
- result.seek(0)
47
-
48
- return result
49
-
50
-
51
29
  @lru_cache
52
30
  def remove_all_widgets(pdf: bytes) -> bytes:
53
31
  """
@@ -63,7 +41,7 @@ def remove_all_widgets(pdf: bytes) -> bytes:
63
41
  Returns:
64
42
  bytes: The PDF with all widgets removed, as a bytes stream.
65
43
  """
66
- pdf_file = PdfReader(stream_to_io(pdf))
44
+ pdf_file = PdfReader(BytesIO(pdf))
67
45
  result_stream = BytesIO()
68
46
  writer = PdfWriter()
69
47
  for page in pdf_file.pages:
@@ -89,7 +67,7 @@ def get_page_streams(pdf: bytes) -> List[bytes]:
89
67
  Returns:
90
68
  List[bytes]: A list of bytes streams, one for each page.
91
69
  """
92
- pdf_file = PdfReader(stream_to_io(pdf))
70
+ pdf_file = PdfReader(BytesIO(pdf))
93
71
  result = []
94
72
 
95
73
  for page in pdf_file.pages:
@@ -167,8 +145,8 @@ def merge_two_pdfs(pdf: bytes, other: bytes) -> bytes:
167
145
  bytes: The merged PDF file as a byte stream.
168
146
  """
169
147
  output = PdfWriter()
170
- pdf_file = PdfReader(stream_to_io(pdf))
171
- other_file = PdfReader(stream_to_io(other))
148
+ pdf_file = PdfReader(BytesIO(pdf))
149
+ other_file = PdfReader(BytesIO(other))
172
150
  result = BytesIO()
173
151
 
174
152
  for page in pdf_file.pages:
@@ -179,7 +157,7 @@ def merge_two_pdfs(pdf: bytes, other: bytes) -> bytes:
179
157
  output.write(result)
180
158
  result.seek(0)
181
159
 
182
- merged_no_widgets = PdfReader(stream_to_io(remove_all_widgets(result.read())))
160
+ merged_no_widgets = PdfReader(BytesIO(remove_all_widgets(result.read())))
183
161
  output = PdfWriter()
184
162
  output.append(merged_no_widgets)
185
163
 
@@ -9,7 +9,6 @@ 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
13
12
  from io import BytesIO
14
13
  from typing import Any, Dict, List, Optional
15
14
 
@@ -18,11 +17,8 @@ from pypdf.generic import ArrayObject, NameObject
18
17
  from reportlab.lib.utils import ImageReader
19
18
  from reportlab.pdfgen.canvas import Canvas
20
19
 
21
- from .assets.blank import BlankPage
22
20
  from .constants import Annots
23
21
  from .patterns import get_widget_key
24
- from .raw.text import RawText
25
- from .utils import stream_to_io
26
22
 
27
23
 
28
24
  def draw_text(canvas: Canvas, **kwargs) -> None:
@@ -43,9 +39,10 @@ def draw_text(canvas: Canvas, **kwargs) -> None:
43
39
  widget = kwargs["widget"]
44
40
  coordinate_x = kwargs["x"]
45
41
  coordinate_y = kwargs["y"]
42
+ font_mapping = kwargs.get("font_mapping", {})
46
43
 
47
44
  text_to_draw = widget.value
48
- canvas.setFont(widget.font, widget.font_size)
45
+ canvas.setFont(font_mapping.get(widget.font, widget.font), widget.font_size)
49
46
  canvas.setFillColorRGB(
50
47
  widget.font_color[0], widget.font_color[1], widget.font_color[2]
51
48
  )
@@ -225,7 +222,9 @@ def draw_image(canvas: Canvas, **kwargs) -> None:
225
222
  image_buff.close()
226
223
 
227
224
 
228
- def create_watermarks_and_draw(pdf: bytes, to_draw: List[dict]) -> List[bytes]:
225
+ def create_watermarks_and_draw(
226
+ pdf: bytes, to_draw: List[dict], font_mapping: Optional[Dict[str, str]] = None
227
+ ) -> List[bytes]:
229
228
  """
230
229
  Creates a watermark PDF for each page of the input PDF based on the drawing instructions.
231
230
 
@@ -238,6 +237,8 @@ def create_watermarks_and_draw(pdf: bytes, to_draw: List[dict]) -> List[bytes]:
238
237
  to_draw (List[dict]): A list of drawing instructions, where each dictionary
239
238
  must contain a "page_number" key (1-based) and a "type" key ("image", "text", or "line")
240
239
  along with type-specific parameters.
240
+ font_mapping (Optional[Dict[str, str]]): A dictionary mapping original font names
241
+ to temporary unique font names used by ReportLab.
241
242
 
242
243
  Returns:
243
244
  List[bytes]: A list of watermark PDF byte streams. An empty byte string (b"")
@@ -258,7 +259,7 @@ def create_watermarks_and_draw(pdf: bytes, to_draw: List[dict]) -> List[bytes]:
258
259
  for each in to_draw:
259
260
  page_to_to_draw[each["page_number"]].append(each)
260
261
 
261
- pdf_file = PdfReader(stream_to_io(pdf))
262
+ pdf_file = PdfReader(BytesIO(pdf))
262
263
  buff = BytesIO()
263
264
 
264
265
  for i, page in enumerate(pdf_file.pages):
@@ -279,7 +280,9 @@ def create_watermarks_and_draw(pdf: bytes, to_draw: List[dict]) -> List[bytes]:
279
280
  )
280
281
 
281
282
  for element in elements:
282
- type_to_func[element["type"]](canvas, **element)
283
+ type_to_func[element["type"]](
284
+ canvas, **element, font_mapping=font_mapping or {}
285
+ )
283
286
 
284
287
  canvas.save()
285
288
  buff.seek(0)
@@ -306,13 +309,13 @@ def merge_watermarks_with_pdf(
306
309
  bytes: A byte stream representing the merged PDF with watermarks applied.
307
310
  """
308
311
  result = BytesIO()
309
- pdf_file = PdfReader(stream_to_io(pdf))
312
+ pdf_file = PdfReader(BytesIO(pdf))
310
313
  output = PdfWriter()
311
314
  output.append(pdf_file)
312
315
 
313
316
  for i, page in enumerate(output.pages):
314
317
  if watermarks[i]:
315
- watermark = PdfReader(stream_to_io(watermarks[i]))
318
+ watermark = PdfReader(BytesIO(watermarks[i]))
316
319
  if watermark.pages:
317
320
  page.merge_page(watermark.pages[0])
318
321
 
@@ -321,26 +324,6 @@ def merge_watermarks_with_pdf(
321
324
  return result.read()
322
325
 
323
326
 
324
- @lru_cache
325
- def get_watermark_with_font(font_name: str) -> bytes:
326
- """
327
- Creates a watermark PDF with a single space character using the specified font.
328
-
329
- This function is primarily used to generate a dummy PDF page that includes
330
- a specific font, which can then be merged with another PDF to ensure the
331
- font is available or embedded. The result is cached for performance.
332
-
333
- Args:
334
- font_name (str): The name of the font to use.
335
-
336
- Returns:
337
- bytes: The watermark PDF as a byte stream.
338
- """
339
- return create_watermarks_and_draw(
340
- BlankPage().read(), [RawText(" ", 1, 0, 0, font=font_name).to_draw]
341
- )[0]
342
-
343
-
344
327
  def _clone_page_widgets(
345
328
  writer: PdfWriter,
346
329
  page: PageObject,
@@ -384,7 +367,7 @@ def _collect_from_single_watermark_specific_page(
384
367
  Dict[int, List[Any]]: A dictionary mapping the first output page (index 0) to cloned widgets.
385
368
  """
386
369
  widgets_to_copy = defaultdict(list)
387
- watermark_reader = PdfReader(stream_to_io(watermark))
370
+ watermark_reader = PdfReader(BytesIO(watermark))
388
371
  if page_num < len(watermark_reader.pages):
389
372
  widgets_to_copy[0] = _clone_page_widgets(
390
373
  writer, watermark_reader.pages[page_num], keys
@@ -409,7 +392,7 @@ def _collect_from_single_watermark_1_to_1(
409
392
  Dict[int, List[Any]]: A dictionary mapping output page indices to cloned widgets.
410
393
  """
411
394
  widgets_to_copy = defaultdict(list)
412
- watermark_reader = PdfReader(stream_to_io(watermark))
395
+ watermark_reader = PdfReader(BytesIO(watermark))
413
396
  for i, page in enumerate(watermark_reader.pages):
414
397
  widgets_to_copy[i] = _clone_page_widgets(writer, page, keys)
415
398
  return widgets_to_copy
@@ -437,7 +420,7 @@ def _collect_from_multiple_watermarks(
437
420
  for i, watermark_stream in enumerate(watermarks):
438
421
  if not watermark_stream:
439
422
  continue
440
- watermark_reader = PdfReader(stream_to_io(watermark_stream))
423
+ watermark_reader = PdfReader(BytesIO(watermark_stream))
441
424
  for j, page in enumerate(watermark_reader.pages):
442
425
  if page_num is None or j == page_num:
443
426
  widgets_to_copy[i].extend(_clone_page_widgets(writer, page, keys))
@@ -525,7 +508,7 @@ def copy_watermark_widgets(
525
508
  bytes: The modified PDF byte stream with copied widgets.
526
509
  """
527
510
  pdf_writer = PdfWriter()
528
- pdf_writer.append(PdfReader(stream_to_io(pdf)))
511
+ pdf_writer.append(PdfReader(BytesIO(pdf)))
529
512
 
530
513
  widgets_to_copy = _collect_widgets_to_copy(pdf_writer, watermarks, keys, page_num)
531
514
  _apply_widgets_to_pages(pdf_writer, widgets_to_copy)
@@ -24,7 +24,6 @@ from reportlab.lib.colors import Color
24
24
  from reportlab.pdfgen.canvas import Canvas
25
25
 
26
26
  from ..constants import fieldFlags, required
27
- from ..utils import stream_to_io
28
27
 
29
28
 
30
29
  class Widget:
@@ -177,7 +176,7 @@ class Widget:
177
176
  """
178
177
  result = []
179
178
 
180
- pdf = PdfReader(stream_to_io(stream))
179
+ pdf = PdfReader(BytesIO(stream))
181
180
  watermark = BytesIO()
182
181
 
183
182
  widgets_by_page = {}
@@ -26,7 +26,6 @@ from reportlab.pdfgen.canvas import Canvas
26
26
  from ..assets.bedrock import BEDROCK_PDF
27
27
  from ..constants import Annots, Rect, T
28
28
  from ..patterns import get_widget_key
29
- from ..utils import stream_to_io
30
29
  from .base import Field
31
30
 
32
31
 
@@ -34,10 +33,11 @@ class SignatureWidget:
34
33
  """
35
34
  Represents a signature widget in a PDF form.
36
35
 
37
- This class is responsible for handling the creation, rendering, and
38
- integration of signature fields in a PDF document. It inherits from
39
- the base Widget class and provides specific functionality for handling
40
- signatures.
36
+ This class is responsible for handling the creation and integration of
37
+ signature fields in a PDF document. Unlike other widget types, it does not
38
+ inherit from the base Widget class instead of using ReportLab's AcroForm
39
+ API, it copies a pre-built signature annotation from a bedrock PDF and
40
+ places it at the specified coordinates.
41
41
 
42
42
  Attributes:
43
43
  OPTIONAL_PARAMS (list): A list of tuples, where each tuple contains the
@@ -111,9 +111,9 @@ class SignatureWidget:
111
111
  for widget in widgets:
112
112
  page_to_widgets[widget.page_number].append(widget)
113
113
 
114
- input_pdf = PdfReader(stream_to_io(stream))
114
+ input_pdf = PdfReader(BytesIO(stream))
115
115
 
116
- bedrock = PdfReader(stream_to_io(BEDROCK_PDF))
116
+ bedrock = PdfReader(BytesIO(BEDROCK_PDF))
117
117
  page = bedrock.pages[0]
118
118
  annot_type_to_annot = {}
119
119
  for annot in page.get(Annots, []): # pylint: disable=E1101
@@ -31,8 +31,8 @@ from .constants import VERSION_IDENTIFIER_PREFIX, VERSION_IDENTIFIERS
31
31
  from .coordinate import generate_coordinate_grid
32
32
  from .egress import appearance_streams_handler, preserve_pdf_properties
33
33
  from .filler import fill
34
- from .font import (get_all_available_fonts, register_font,
35
- register_font_acroform)
34
+ from .font import (get_all_available_fonts, register_font_acroform,
35
+ temporary_font_registration, validate_font)
36
36
  from .hooks import trigger_widget_hooks
37
37
  from .middleware.dropdown import Dropdown
38
38
  from .middleware.signature import Signature
@@ -765,9 +765,10 @@ class PdfWrapper:
765
765
  PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
766
766
  """
767
767
 
768
- watermarks = create_watermarks_and_draw(
769
- self._read(), [each.to_draw for each in elements]
770
- )
768
+ with temporary_font_registration(self._font_register_events) as font_mapping:
769
+ watermarks = create_watermarks_and_draw(
770
+ self._read(), [each.to_draw for each in elements], font_mapping
771
+ )
771
772
 
772
773
  stream_with_widgets = self._read()
773
774
  self._stream = merge_watermarks_with_pdf(self._read(), watermarks)
@@ -801,9 +802,9 @@ class PdfWrapper:
801
802
 
802
803
  ttf_file = fp_or_f_obj_or_stream_to_stream(ttf_file)
803
804
 
804
- if register_font(font_name, ttf_file) if ttf_file is not None else False:
805
+ if validate_font(font_name, ttf_file) if ttf_file is not None else False:
805
806
  self._stream, new_font_name = register_font_acroform(
806
- self._read(), font_name, ttf_file, getattr(self, "need_appearances")
807
+ self._read(), ttf_file, getattr(self, "need_appearances")
807
808
  )
808
809
  self._available_fonts[font_name] = new_font_name
809
810
  self._font_register_events.append((font_name, ttf_file))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.7.3
3
+ Version: 4.7.5
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -24,7 +24,7 @@ Requires-Dist: cryptography<47.0.0,>=46.0.3
24
24
  Requires-Dist: fonttools<5.0.0,>=4.60.1
25
25
  Requires-Dist: pikepdf<11.0.0,>=10.5.0
26
26
  Requires-Dist: pillow<13.0.0,>=12.0.0
27
- Requires-Dist: pypdf<7.0.0,>=6.3.0
27
+ Requires-Dist: pypdf<7.0.0,>=6.9.0
28
28
  Requires-Dist: reportlab<5.0.0,>=4.4.6
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: black<27.0.0,>=25.11.0; extra == "dev"
@@ -2,7 +2,7 @@ cryptography<47.0.0,>=46.0.3
2
2
  fonttools<5.0.0,>=4.60.1
3
3
  pikepdf<11.0.0,>=10.5.0
4
4
  pillow<13.0.0,>=12.0.0
5
- pypdf<7.0.0,>=6.3.0
5
+ pypdf<7.0.0,>=6.9.0
6
6
  reportlab<5.0.0,>=4.4.6
7
7
 
8
8
  [dev]
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "fonttools>=4.60.1,<5.0.0",
32
32
  "pikepdf>=10.5.0,<11.0.0",
33
33
  "pillow>=12.0.0,<13.0.0",
34
- "pypdf>=6.3.0,<7.0.0",
34
+ "pypdf>=6.9.0,<7.0.0",
35
35
  "reportlab>=4.4.6,<5.0.0",
36
36
  ]
37
37
 
@@ -136,5 +136,5 @@ include = ["PyPDFForm*"]
136
136
 
137
137
  [tool.pytest.ini_options]
138
138
  markers = [
139
- "posix_only",
139
+ "posix_only", # mainly because of zlib vs zlib-ng
140
140
  ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes