PyPDFForm 2.2.2__tar.gz → 2.2.3__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.

Potentially problematic release.


This version of PyPDFForm might be problematic. Click here for more details.

Files changed (53) hide show
  1. {pypdfform-2.2.2 → pypdfform-2.2.3}/PKG-INFO +1 -1
  2. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/watermark.py +21 -11
  4. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/wrapper.py +14 -5
  5. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm.egg-info/PKG-INFO +1 -1
  6. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_functional.py +113 -0
  7. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_paragraph.py +96 -0
  8. {pypdfform-2.2.2 → pypdfform-2.2.3}/LICENSE +0 -0
  9. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/adapter.py +0 -0
  10. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/constants.py +0 -0
  11. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/coordinate.py +0 -0
  12. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/filler.py +0 -0
  13. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/font.py +0 -0
  14. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/image.py +0 -0
  15. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/middleware/__init__.py +0 -0
  16. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/middleware/base.py +0 -0
  17. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/middleware/checkbox.py +0 -0
  18. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/middleware/dropdown.py +0 -0
  19. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/middleware/image.py +0 -0
  20. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/middleware/radio.py +0 -0
  21. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/middleware/signature.py +0 -0
  22. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/middleware/text.py +0 -0
  23. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/patterns.py +0 -0
  24. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/template.py +0 -0
  25. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/utils.py +0 -0
  26. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/widgets/__init__.py +0 -0
  27. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/widgets/base.py +0 -0
  28. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/widgets/bedrock.py +0 -0
  29. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/widgets/checkbox.py +0 -0
  30. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/widgets/dropdown.py +0 -0
  31. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/widgets/image.py +0 -0
  32. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/widgets/radio.py +0 -0
  33. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/widgets/signature.py +0 -0
  34. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm/widgets/text.py +0 -0
  35. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  36. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  37. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm.egg-info/requires.txt +0 -0
  38. {pypdfform-2.2.2 → pypdfform-2.2.3}/PyPDFForm.egg-info/top_level.txt +0 -0
  39. {pypdfform-2.2.2 → pypdfform-2.2.3}/README.md +0 -0
  40. {pypdfform-2.2.2 → pypdfform-2.2.3}/pyproject.toml +0 -0
  41. {pypdfform-2.2.2 → pypdfform-2.2.3}/setup.cfg +0 -0
  42. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_adobe_mode.py +0 -0
  43. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_create_widget.py +0 -0
  44. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_dropdown.py +0 -0
  45. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_dropdown_simple.py +0 -0
  46. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_fill_max_length_text_field.py +0 -0
  47. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_fill_max_length_text_field_simple.py +0 -0
  48. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_fill_method.py +0 -0
  49. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_functional_simple.py +0 -0
  50. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_paragraph_simple.py +0 -0
  51. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_preview.py +0 -0
  52. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_signature.py +0 -0
  53. {pypdfform-2.2.2 → pypdfform-2.2.3}/tests/test_use_full_widget_name.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -5,7 +5,7 @@ This package provides tools for filling PDF forms, drawing text and images,
5
5
  and manipulating PDF form elements programmatically.
6
6
  """
7
7
 
8
- __version__ = "2.2.2"
8
+ __version__ = "2.2.3"
9
9
 
10
10
  from .wrapper import FormWrapper, PdfWrapper
11
11
 
@@ -10,7 +10,7 @@ This module handles:
10
10
  """
11
11
 
12
12
  from io import BytesIO
13
- from typing import List
13
+ from typing import List, Union
14
14
 
15
15
  from pypdf import PdfReader, PdfWriter
16
16
  from pypdf.generic import ArrayObject, NameObject
@@ -335,20 +335,22 @@ def merge_watermarks_with_pdf(
335
335
 
336
336
 
337
337
  def copy_watermark_widgets(
338
- pdf: bytes, watermarks: List[bytes], keys: List[str]
338
+ pdf: bytes, watermarks: Union[List[bytes], bytes], keys: Union[List[str], None]
339
339
  ) -> bytes:
340
340
  """
341
341
  Copies annotation widgets (form fields) from watermark PDFs onto the corresponding pages of a base PDF,
342
342
  including only those widgets whose key matches an entry in the provided keys list.
343
343
 
344
- For each watermark in the provided list, any annotation widgets (such as form fields) are cloned
345
- and appended to the annotations of the corresponding page in the base PDF, but only if their key
346
- matches one of the specified keys.
344
+ For each watermark in the provided list (or single watermark PDF if bytes input is provided),
345
+ any annotation widgets (such as form fields) are cloned and appended to the annotations of the
346
+ corresponding page in the base PDF, but only if their key matches one of the specified keys.
347
347
 
348
348
  Args:
349
349
  pdf: The original PDF document as bytes.
350
- watermarks: List of watermark PDF data (as bytes), one per page. Empty or None entries are skipped.
350
+ watermarks: Either a list of watermark PDF data (as bytes, one per page) or a single watermark PDF.
351
+ Empty or None entries are skipped.
351
352
  keys: List of widget keys (str). Only widgets whose key is in this list will be copied.
353
+ If None, all widgets will be copied.
352
354
 
353
355
  Returns:
354
356
  bytes: The resulting PDF document with selected annotation widgets from watermarks copied onto their respective pages.
@@ -358,21 +360,29 @@ def copy_watermark_widgets(
358
360
  out = PdfWriter()
359
361
  out.append(pdf_file)
360
362
 
361
- widgets_to_copy = {}
363
+ widgets_to_copy_watermarks = {}
364
+ widgets_to_copy_pdf = {}
365
+
366
+ widgets_to_copy = widgets_to_copy_watermarks
367
+ if isinstance(watermarks, bytes):
368
+ watermarks = [watermarks]
369
+ widgets_to_copy = widgets_to_copy_pdf
362
370
 
363
371
  for i, watermark in enumerate(watermarks):
364
372
  if not watermark:
365
373
  continue
366
374
 
367
- widgets_to_copy[i] = []
375
+ widgets_to_copy_watermarks[i] = []
368
376
  watermark_file = PdfReader(stream_to_io(watermark))
369
- for page in watermark_file.pages:
377
+ for j, page in enumerate(watermark_file.pages):
378
+ widgets_to_copy_pdf[j] = []
370
379
  for annot in page.get(Annots, []): # noqa
371
380
  key = extract_widget_property(
372
381
  annot.get_object(), WIDGET_KEY_PATTERNS, None, str
373
382
  )
374
- if key in keys:
375
- widgets_to_copy[i].append(annot.clone(out))
383
+ if keys is None or key in keys:
384
+ widgets_to_copy_watermarks[i].append(annot.clone(out))
385
+ widgets_to_copy_pdf[j].append(annot.clone(out))
376
386
 
377
387
  for i, page in enumerate(out.pages):
378
388
  if i in widgets_to_copy:
@@ -586,7 +586,10 @@ class PdfWrapper(FormWrapper):
586
586
  """Draws static text onto the PDF document at specified coordinates.
587
587
 
588
588
  Adds non-interactive text that becomes part of the PDF content rather
589
- than a form field. Useful for annotations, labels, signatures, etc.
589
+ than a form field. The text is drawn using a temporary Text widget and
590
+ merged via watermark operations, preserving existing form fields.
591
+
592
+ Supports multi-line text (using NEW_LINE_SYMBOL) and custom formatting.
590
593
 
591
594
  Args:
592
595
  text: The text content to draw (supports newlines with NEW_LINE_SYMBOL)
@@ -624,7 +627,11 @@ class PdfWrapper(FormWrapper):
624
627
  ],
625
628
  )
626
629
 
630
+ stream_with_widgets = self.read()
627
631
  self.stream = merge_watermarks_with_pdf(self.stream, watermarks)
632
+ self.stream = copy_watermark_widgets(
633
+ remove_all_widgets(self.stream), stream_with_widgets, None
634
+ )
628
635
 
629
636
  return self
630
637
 
@@ -640,10 +647,8 @@ class PdfWrapper(FormWrapper):
640
647
  ) -> PdfWrapper:
641
648
  """Draws an image onto the PDF document at specified coordinates.
642
649
 
643
- Supports common image formats (JPEG, PNG) from various sources:
644
- - Raw image bytes
645
- - File path
646
- - File-like object
650
+ The image is merged via watermark operations, preserving existing form fields.
651
+ Supports common formats (JPEG, PNG) from bytes, file paths, or file objects.
647
652
 
648
653
  Args:
649
654
  image: Image data as bytes, file path, or file object
@@ -664,7 +669,11 @@ class PdfWrapper(FormWrapper):
664
669
  self.stream, page_number, "image", [[image, x, y, width, height]]
665
670
  )
666
671
 
672
+ stream_with_widgets = self.read()
667
673
  self.stream = merge_watermarks_with_pdf(self.stream, watermarks)
674
+ self.stream = copy_watermark_widgets(
675
+ remove_all_widgets(self.stream), stream_with_widgets, None
676
+ )
668
677
 
669
678
  return self
670
679
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -34,6 +34,25 @@ def test_fill(template_stream, pdf_samples, data_dict, request):
34
34
  assert not widgets
35
35
 
36
36
 
37
+ def test_fill_not_render_widgets(template_stream, pdf_samples, data_dict, request):
38
+ expected_path = os.path.join(pdf_samples, "test_fill_not_render_widgets.pdf")
39
+ with open(expected_path, "rb+") as f:
40
+ obj = PdfWrapper(template_stream, render_widgets=False).fill(
41
+ data_dict,
42
+ )
43
+
44
+ request.config.results["expected_path"] = expected_path
45
+ request.config.results["stream"] = obj.read()
46
+
47
+ assert len(obj.read()) == len(obj.stream)
48
+ assert obj.read() == obj.stream
49
+
50
+ expected = f.read()
51
+
52
+ assert len(obj.stream) == len(expected)
53
+ assert obj.stream == expected
54
+
55
+
37
56
  def test_register_bad_fonts():
38
57
  assert not PdfWrapper.register_font("foo", b"foo")
39
58
  assert not PdfWrapper.register_font("foo", "foo")
@@ -218,6 +237,52 @@ def test_draw_text_on_one_page(template_stream, pdf_samples, request):
218
237
  assert obj.stream == expected
219
238
 
220
239
 
240
+ def test_draw_text_on_radio_template(
241
+ template_with_radiobutton_stream, pdf_samples, request
242
+ ):
243
+ expected_path = os.path.join(pdf_samples, "test_draw_text_on_radio_template.pdf")
244
+ with open(expected_path, "rb+") as f:
245
+ obj = PdfWrapper(template_with_radiobutton_stream).draw_text(
246
+ "drawn_text",
247
+ 1,
248
+ 300,
249
+ 225,
250
+ font=constants.DEFAULT_FONT,
251
+ font_size=20,
252
+ font_color=(1, 0, 0),
253
+ )
254
+
255
+ request.config.results["expected_path"] = expected_path
256
+ request.config.results["stream"] = obj.read()
257
+
258
+ expected = f.read()
259
+
260
+ assert len(obj.stream) == len(expected)
261
+ assert obj.stream == expected
262
+
263
+
264
+ def test_draw_text_on_sejda_template(sejda_template, pdf_samples, request):
265
+ expected_path = os.path.join(pdf_samples, "test_draw_text_on_sejda_template.pdf")
266
+ with open(expected_path, "rb+") as f:
267
+ obj = PdfWrapper(sejda_template).draw_text(
268
+ "drawn_text",
269
+ 1,
270
+ 300,
271
+ 225,
272
+ font=constants.DEFAULT_FONT,
273
+ font_size=20,
274
+ font_color=(1, 0, 0),
275
+ )
276
+
277
+ request.config.results["expected_path"] = expected_path
278
+ request.config.results["stream"] = obj.read()
279
+
280
+ expected = f.read()
281
+
282
+ assert len(obj.stream) == len(expected)
283
+ assert obj.stream == expected
284
+
285
+
221
286
  def test_draw_text_new_line_symbol(template_stream, pdf_samples, request):
222
287
  expected_path = os.path.join(
223
288
  pdf_samples, "sample_pdf_with_drawn_text_new_line_symbol.pdf"
@@ -261,6 +326,54 @@ def test_draw_image_on_one_page(template_stream, image_samples, pdf_samples, req
261
326
  assert obj.stream == expected
262
327
 
263
328
 
329
+ def test_draw_image_on_radio_template(
330
+ template_with_radiobutton_stream, image_samples, pdf_samples, request
331
+ ):
332
+ expected_path = os.path.join(pdf_samples, "test_draw_image_on_radio_template.pdf")
333
+ with open(expected_path, "rb+") as f:
334
+ with open(os.path.join(image_samples, "sample_image.jpg"), "rb+") as _f:
335
+ obj = PdfWrapper(template_with_radiobutton_stream).draw_image(
336
+ _f,
337
+ 2,
338
+ 100,
339
+ 100,
340
+ 400,
341
+ 225,
342
+ )
343
+
344
+ expected = f.read()
345
+
346
+ request.config.results["expected_path"] = expected_path
347
+ request.config.results["stream"] = obj.read()
348
+
349
+ assert len(obj.stream) == len(expected)
350
+ assert obj.stream == expected
351
+
352
+
353
+ def test_draw_image_on_sejda_template(
354
+ sejda_template, image_samples, pdf_samples, request
355
+ ):
356
+ expected_path = os.path.join(pdf_samples, "test_draw_image_on_sejda_template.pdf")
357
+ with open(expected_path, "rb+") as f:
358
+ with open(os.path.join(image_samples, "sample_image.jpg"), "rb+") as _f:
359
+ obj = PdfWrapper(sejda_template).draw_image(
360
+ _f,
361
+ 2,
362
+ 100,
363
+ 100,
364
+ 400,
365
+ 225,
366
+ )
367
+
368
+ expected = f.read()
369
+
370
+ request.config.results["expected_path"] = expected_path
371
+ request.config.results["stream"] = obj.read()
372
+
373
+ assert len(obj.stream) == len(expected)
374
+ assert obj.stream == expected
375
+
376
+
264
377
  def test_draw_png_image_on_one_page(
265
378
  template_stream, image_samples, pdf_samples, request
266
379
  ):
@@ -139,6 +139,102 @@ def test_fill_sejda_complex(sejda_template_complex, pdf_samples, request):
139
139
  assert obj.stream == expected
140
140
 
141
141
 
142
+ def test_fill_sejda_complex_not_render_widgets(
143
+ sejda_template_complex, pdf_samples, request
144
+ ):
145
+ expected_path = os.path.join(
146
+ pdf_samples, "paragraph", "test_fill_sejda_complex_not_render_widgets.pdf"
147
+ )
148
+ with open(expected_path, "rb+") as f:
149
+ obj = PdfWrapper(sejda_template_complex, render_widgets=False).fill(
150
+ {
151
+ "checkbox": True,
152
+ "radio": 0,
153
+ "dropdown_font_auto_left": 0,
154
+ "dropdown_font_auto_center": 1,
155
+ "dropdown_font_auto_right": 2,
156
+ "dropdown_font_ten_left": 0,
157
+ "dropdown_font_ten_center": 1,
158
+ "dropdown_font_ten_right": 2,
159
+ "paragraph_font_auto_left": "paragraph_font_auto_left",
160
+ "paragraph_font_auto_center": "paragraph_font_auto_center",
161
+ "paragraph_font_auto_right": "paragraph_font_auto_right",
162
+ "paragraph_font_ten_left": "paragraph_font_ten_left",
163
+ "paragraph_font_ten_center": "paragraph_font_ten_center",
164
+ "paragraph_font_ten_right": "paragraph_font_ten_right",
165
+ "text__font_auto_left": "test text",
166
+ "text_font_auto_center": "test text",
167
+ "text_font_auto_right": "test text",
168
+ "text_font_ten_left": "text_font_ten_left",
169
+ "text_font_ten_center": "text_font_ten_center",
170
+ "text_font_ten_right": "text_font_ten_right",
171
+ }
172
+ )
173
+
174
+ request.config.results["expected_path"] = expected_path
175
+ request.config.results["stream"] = obj.read()
176
+ assert len(obj.read()) == len(obj.stream)
177
+ assert obj.read() == obj.stream
178
+
179
+ expected = f.read()
180
+
181
+ assert len(obj.stream) == len(expected)
182
+ assert obj.stream == expected
183
+
184
+
185
+ def test_fill_sejda_complex_not_render_some_widgets(
186
+ sejda_template_complex, pdf_samples, request
187
+ ):
188
+ expected_path = os.path.join(
189
+ pdf_samples, "paragraph", "test_fill_sejda_complex_not_render_some_widgets.pdf"
190
+ )
191
+ with open(expected_path, "rb+") as f:
192
+ obj = PdfWrapper(sejda_template_complex)
193
+
194
+ obj.widgets["radio"].render_widget = False
195
+ obj.widgets["dropdown_font_auto_center"].render_widget = False
196
+ obj.widgets["dropdown_font_ten_center"].render_widget = False
197
+ obj.widgets["paragraph_font_auto_center"].render_widget = False
198
+ obj.widgets["paragraph_font_ten_center"].render_widget = False
199
+ obj.widgets["text_font_auto_center"].render_widget = False
200
+ obj.widgets["text_font_ten_center"].render_widget = False
201
+
202
+ obj.fill(
203
+ {
204
+ "checkbox": True,
205
+ "radio": 0,
206
+ "dropdown_font_auto_left": 0,
207
+ "dropdown_font_auto_center": 1,
208
+ "dropdown_font_auto_right": 2,
209
+ "dropdown_font_ten_left": 0,
210
+ "dropdown_font_ten_center": 1,
211
+ "dropdown_font_ten_right": 2,
212
+ "paragraph_font_auto_left": "paragraph_font_auto_left",
213
+ "paragraph_font_auto_center": "paragraph_font_auto_center",
214
+ "paragraph_font_auto_right": "paragraph_font_auto_right",
215
+ "paragraph_font_ten_left": "paragraph_font_ten_left",
216
+ "paragraph_font_ten_center": "paragraph_font_ten_center",
217
+ "paragraph_font_ten_right": "paragraph_font_ten_right",
218
+ "text__font_auto_left": "test text",
219
+ "text_font_auto_center": "test text",
220
+ "text_font_auto_right": "test text",
221
+ "text_font_ten_left": "text_font_ten_left",
222
+ "text_font_ten_center": "text_font_ten_center",
223
+ "text_font_ten_right": "text_font_ten_right",
224
+ }
225
+ )
226
+
227
+ request.config.results["expected_path"] = expected_path
228
+ request.config.results["stream"] = obj.read()
229
+ assert len(obj.read()) == len(obj.stream)
230
+ assert obj.read() == obj.stream
231
+
232
+ expected = f.read()
233
+
234
+ assert len(obj.stream) == len(expected)
235
+ assert obj.stream == expected
236
+
237
+
142
238
  def test_sejda_complex_paragraph_multiple_line_alignment(
143
239
  sejda_template_complex, pdf_samples, request
144
240
  ):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes