PyPDFForm 3.5.3__tar.gz → 3.5.4__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 (52) hide show
  1. {pypdfform-3.5.3 → pypdfform-3.5.4}/PKG-INFO +3 -3
  2. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/adapter.py +0 -1
  4. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/coordinate.py +0 -2
  5. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/filler.py +0 -5
  6. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/font.py +0 -5
  7. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/hooks.py +18 -25
  8. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/image.py +0 -3
  9. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/middleware/base.py +3 -4
  10. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/middleware/signature.py +0 -1
  11. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/patterns.py +0 -4
  12. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/template.py +1 -6
  13. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/utils.py +0 -8
  14. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/watermark.py +0 -7
  15. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/widgets/base.py +0 -3
  16. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/widgets/radio.py +0 -2
  17. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/widgets/signature.py +0 -4
  18. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/wrapper.py +0 -11
  19. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm.egg-info/PKG-INFO +3 -3
  20. {pypdfform-3.5.3 → pypdfform-3.5.4}/pyproject.toml +7 -2
  21. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_adobe_mode.py +3 -0
  22. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_create_widget.py +39 -0
  23. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_dropdown.py +11 -9
  24. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_font_widths.py +3 -1
  25. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_functional.py +20 -12
  26. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_paragraph.py +26 -24
  27. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_signature.py +9 -3
  28. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_widget_attr_trigger.py +26 -0
  29. {pypdfform-3.5.3 → pypdfform-3.5.4}/LICENSE +0 -0
  30. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/constants.py +0 -0
  31. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/middleware/__init__.py +0 -0
  32. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/middleware/checkbox.py +0 -0
  33. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/middleware/dropdown.py +0 -0
  34. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/middleware/image.py +0 -0
  35. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/middleware/radio.py +0 -0
  36. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/middleware/text.py +0 -0
  37. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/widgets/__init__.py +0 -0
  38. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/widgets/bedrock.py +0 -0
  39. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/widgets/checkbox.py +0 -0
  40. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/widgets/dropdown.py +0 -0
  41. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/widgets/image.py +0 -0
  42. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm/widgets/text.py +0 -0
  43. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  44. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  45. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm.egg-info/requires.txt +0 -0
  46. {pypdfform-3.5.3 → pypdfform-3.5.4}/PyPDFForm.egg-info/top_level.txt +0 -0
  47. {pypdfform-3.5.3 → pypdfform-3.5.4}/README.md +0 -0
  48. {pypdfform-3.5.3 → pypdfform-3.5.4}/setup.cfg +0 -0
  49. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_extract_values.py +0 -0
  50. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_fill_max_length_text_field.py +0 -0
  51. {pypdfform-3.5.3 → pypdfform-3.5.4}/tests/test_fill_method.py +0 -0
  52. {pypdfform-3.5.3 → pypdfform-3.5.4}/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: 3.5.3
3
+ Version: 3.5.4
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -10,14 +10,14 @@ Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3 :: Only
13
- Classifier: Programming Language :: Python :: 3.9
14
13
  Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
17
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
18
  Classifier: Operating System :: OS Independent
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
- Requires-Python: >=3.9
20
+ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: cryptography
@@ -20,7 +20,7 @@ The library supports various PDF form features, including:
20
20
  PyPDFForm aims to simplify PDF form manipulation, making it accessible to developers of all skill levels.
21
21
  """
22
22
 
23
- __version__ = "3.5.3"
23
+ __version__ = "3.5.4"
24
24
 
25
25
  from .middleware.text import Text # exposing for setting global font attrs
26
26
  from .widgets import Fields
@@ -9,7 +9,6 @@ filling operations, where the input PDF template can be provided in different
9
9
  forms. The module ensures that the input is properly converted into a byte
10
10
  stream before further processing.
11
11
  """
12
- # TODO: For large PDF files, reading the entire file into memory using `_file.read()` in `fp_or_f_obj_or_stream_to_stream` can be inefficient. Consider streaming or chunking if downstream processing allows.
13
12
 
14
13
  from os.path import isfile
15
14
  from typing import Any, BinaryIO, Union
@@ -6,8 +6,6 @@ This module provides functionality to generate coordinate grids on existing PDF
6
6
  It allows developers to visualize the coordinate system of each page in a PDF, which can be helpful
7
7
  for debugging and precisely positioning elements when filling or drawing on PDF forms.
8
8
  """
9
- # TODO: The `PdfReader` object is initialized twice (lines 42 and implicitly within `create_watermarks_and_draw` if it re-reads the PDF). Consider initializing it once and passing the object or its relevant parts to avoid redundant parsing, especially for large PDFs.
10
- # TODO: Drawing operations for lines and texts are performed and merged separately. It might be more efficient to combine all drawing operations for a page into a single `create_watermarks_and_draw` call or to merge all watermarks in one final step to reduce PDF processing overhead.
11
9
 
12
10
  from typing import Tuple
13
11
 
@@ -7,11 +7,6 @@ It includes functions for handling various form field types, such as text fields
7
7
  checkboxes, radio buttons, dropdowns, images, and signatures. The module also
8
8
  supports flattening the filled form to prevent further modifications.
9
9
  """
10
- # TODO: In `fill` function, `PdfReader(stream_to_io(template))` and `out.append(pdf)` might involve re-parsing or copying the entire PDF. For very large PDFs, consider if `pypdf` offers more efficient ways to modify in-place or stream processing.
11
- # TODO: The `get_widget_key` function is called repeatedly in a loop. If its internal logic is complex, consider caching its results or optimizing its implementation to avoid redundant computations.
12
- # TODO: The `signature_image_handler` function involves `get_image_dimensions` and `get_draw_image_resolutions`. If image processing is a bottleneck, consider optimizing these image-related operations, perhaps by using faster image libraries or pre-calculating dimensions if images are reused.
13
- # TODO: Similar to `coordinate.py`, `get_drawn_stream` involves multiple `create_watermarks_and_draw` and `merge_watermarks_with_pdf` calls. Combining drawing operations or merging watermarks in a single pass could reduce overhead.
14
- # TODO: The `radio_button_tracker` logic involves iterating through all radio buttons. For forms with many radio buttons, consider optimizing the lookup or update mechanism if performance becomes an issue.
15
10
 
16
11
  from io import BytesIO
17
12
  from typing import Dict, Union, cast
@@ -6,11 +6,6 @@ It includes functions for registering fonts with ReportLab and within the PDF's
6
6
  allowing these fonts to be used when filling form fields. The module also provides utilities
7
7
  for extracting font information from TTF streams and managing font names within a PDF.
8
8
  """
9
- # TODO: In `get_additional_font_params`, iterating through `reader.pages[0][Resources][Font].values()` can be inefficient for PDFs with many fonts. Consider building a font lookup dictionary once per PDF or caching results if this function is called frequently with the same PDF.
10
- # TODO: In `register_font_acroform`, `PdfReader(stream_to_io(pdf))` and `writer.append(reader)` involve re-parsing and appending the PDF. For large PDFs, passing `PdfReader` and `PdfWriter` objects directly could reduce overhead.
11
- # TODO: In `register_font_acroform`, `compress(ttf_stream)` can be CPU-intensive. If the same font stream is registered multiple times within a single PDF processing session, consider caching the compressed stream to avoid redundant compression.
12
- # TODO: In `get_new_font_name`, while `existing` is a set, if `n` needs to increment many times due to a dense range of existing font names, the `while` loop could be slow. However, this is likely a minor bottleneck in typical scenarios.
13
- # TODO: In `get_all_available_fonts`, the `replace("/", "")` operation on `BaseFont` could be avoided if font names are consistently handled with or without the leading slash to prevent string manipulation overhead in a loop.
14
9
 
15
10
  from functools import lru_cache
16
11
  from io import BytesIO
@@ -8,10 +8,6 @@ of checkbox and radio button widgets. It also provides functions for flattening
8
8
  generic and radio button widgets. These hooks are triggered during the PDF form
9
9
  filling process, allowing for customization of the form's appearance and behavior.
10
10
  """
11
- # TODO: In `trigger_widget_hooks`, the PDF is read and written in each call. If this function is part of a larger workflow, consider passing `PdfReader` and `PdfWriter` objects to avoid redundant parsing and writing, allowing modifications to be accumulated and written once.
12
- # TODO: String manipulations (split/join) in `update_text_field_font`, `update_text_field_font_size`, and `update_text_field_font_color` could be optimized for very long `DA` strings, potentially using more efficient string manipulation techniques or regex if the structure is consistent.
13
- # TODO: The `get_widget_key` function is called in a loop within `trigger_widget_hooks`. If its internal logic is complex, consider caching its results or optimizing its implementation to avoid redundant computations.
14
- # TODO: In `flatten_radio` and `flatten_generic`, `annot.get(NameObject(Ff), 0)` is called twice within the conditional. Store this value in a local variable to avoid redundant dictionary lookups.
15
11
 
16
12
  import sys
17
13
  from io import BytesIO
@@ -216,9 +212,7 @@ def update_text_field_multiline(annot: DictionaryObject, val: bool) -> None:
216
212
  val (bool): True to enable multiline, False to disable.
217
213
  """
218
214
  if val:
219
- # TODO: investigate this more
220
- # may need to change everywhere how feature flags precedence work
221
- # https://github.com/chinapandaman/PyPDFForm/issues/1162#issuecomment-3326233842
215
+ # Ff in annot[Parent] only in hooks.py, or when editing instead of retrieving
222
216
  if Parent in annot and Ff in annot[Parent]:
223
217
  annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
224
218
  int(
@@ -247,7 +241,7 @@ def update_text_field_comb(annot: DictionaryObject, val: bool) -> None:
247
241
  val (bool): True to enable comb, False to disable.
248
242
  """
249
243
  if val:
250
- if Parent in annot and Ff not in annot:
244
+ if Parent in annot and Ff in annot[Parent]:
251
245
  annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
252
246
  int(
253
247
  annot[NameObject(Parent)][NameObject(Ff)]
@@ -367,7 +361,7 @@ def flatten_generic(annot: DictionaryObject, val: bool) -> None:
367
361
  annot (DictionaryObject): The annotation dictionary.
368
362
  val (bool): True to flatten (make read-only), False to unflatten (make editable).
369
363
  """
370
- if Parent in annot and Ff not in annot:
364
+ if Parent in annot and (Ff in annot[Parent] or Ff not in annot):
371
365
  annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
372
366
  (
373
367
  int(annot.get(NameObject(Ff), 0)) | READ_ONLY
@@ -412,20 +406,19 @@ def update_field_required(annot: DictionaryObject, val: bool) -> None:
412
406
  annot (DictionaryObject): The annotation dictionary for the form field.
413
407
  val (bool): True to set the field as required, False to make it optional.
414
408
  """
415
- # TODO: add a test case when supporting edit required
416
- # if Parent in annot and Ff not in annot:
417
- # annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
418
- # (
419
- # int(annot.get(NameObject(Ff), 0)) | REQUIRED
420
- # if val
421
- # else int(annot.get(NameObject(Ff), 0)) & ~REQUIRED
422
- # )
423
- # )
424
- # else:
425
- annot[NameObject(Ff)] = NumberObject(
426
- (
427
- int(annot.get(NameObject(Ff), 0)) | REQUIRED
428
- if val
429
- else int(annot.get(NameObject(Ff), 0)) & ~REQUIRED
409
+ if Parent in annot and Ff in annot[Parent]:
410
+ annot[NameObject(Parent)][NameObject(Ff)] = NumberObject(
411
+ (
412
+ int(annot.get(NameObject(Ff), 0)) | REQUIRED
413
+ if val
414
+ else int(annot.get(NameObject(Ff), 0)) & ~REQUIRED
415
+ )
416
+ )
417
+ else:
418
+ annot[NameObject(Ff)] = NumberObject(
419
+ (
420
+ int(annot.get(NameObject(Ff), 0)) | REQUIRED
421
+ if val
422
+ else int(annot.get(NameObject(Ff), 0)) & ~REQUIRED
423
+ )
430
424
  )
431
- )
@@ -6,9 +6,6 @@ It includes functions for rotating images, retrieving image dimensions, and
6
6
  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
- # TODO: In `rotate_image` and `get_image_dimensions`, `BytesIO` is used to wrap the image stream. While necessary for PIL, consider if the `image_stream` is already a file-like object in some calling contexts, which could avoid redundant copying to `BytesIO`.
10
- # TODO: The `rotate_image` function creates a new `BytesIO` object and saves the image to it. For multiple rotations or image manipulations, consider keeping the `PIL.Image.Image` object in memory and performing operations on it directly before a final save to bytes, to avoid repeated I/O operations.
11
- # TODO: The `get_image_dimensions` function opens the image to get its size. If image dimensions are frequently needed for the same image, consider caching the dimensions to avoid re-opening and re-parsing the image data.
12
9
 
13
10
  from io import BytesIO
14
11
  from typing import Tuple, Union
@@ -42,8 +42,7 @@ class Widget:
42
42
  super().__init__()
43
43
  self._name = name
44
44
  self._value = value
45
- self.desc: str = None
46
- self.tooltip: str = None # TODO: sync tooltip and desc
45
+ self.tooltip: str = None
47
46
  self.readonly: bool = None
48
47
  self.required: bool = None
49
48
  self.hooks_to_trigger: list = []
@@ -107,8 +106,8 @@ class Widget:
107
106
  """
108
107
  result = {}
109
108
 
110
- if self.desc is not None:
111
- result["description"] = self.desc
109
+ if self.tooltip is not None:
110
+ result["description"] = self.tooltip
112
111
 
113
112
  return result
114
113
 
@@ -6,7 +6,6 @@ This module defines the Signature class, which is a subclass of the
6
6
  Widget class. It represents a signature form field in a PDF document,
7
7
  allowing users to add their signature as an image.
8
8
  """
9
- # TODO: In the `stream` property, `fp_or_f_obj_or_stream_to_stream` is called every time the property is accessed. If the signature image is large or the property is accessed frequently, consider caching the result of `fp_or_f_obj_or_stream_to_stream` to avoid redundant file reads.
10
9
 
11
10
  from os.path import expanduser
12
11
  from typing import Union
@@ -7,10 +7,6 @@ checkboxes, radio buttons, dropdowns, images, and signatures) based on their
7
7
  properties in the PDF's annotation dictionary. It also provides utility functions
8
8
  for updating these widgets.
9
9
  """
10
- # TODO: The `WIDGET_TYPE_PATTERNS` list is iterated through to determine widget types. For very large numbers of annotations or complex pattern matching, consider optimizing this lookup, perhaps by pre-compiling regexes or using a more efficient data structure if the patterns allow.
11
- # TODO: In `update_checkbox_value` and `update_radio_value`, iterating through `annot[AP][N]` to find the correct appearance state might be slow if `N` contains many entries. If possible, a direct lookup or a more optimized search could improve performance.
12
- # TODO: In `update_dropdown_value`, the list comprehension for `ArrayObject` can be computationally intensive for dropdowns with many choices, as it creates new `TextStringObject` and `ArrayObject` instances for each choice. Consider optimizing this if dropdowns have a very large number of options.
13
- # TODO: The `get_checkbox_value` and `get_radio_value` functions involve dictionary lookups and comparisons. While generally fast, repeated calls in a tight loop for many widgets could accumulate overhead.
14
10
 
15
11
  from typing import Union
16
12
 
@@ -7,11 +7,6 @@ in PDF form templates. It leverages the pypdf library for PDF manipulation
7
7
  and defines specific patterns for identifying and constructing different
8
8
  types of widgets.
9
9
  """
10
- # TODO: In `build_widgets`, the `get_widgets_by_page` function is called, which then iterates through pages and annotations. For very large PDFs, this initial parsing and iteration can be a bottleneck. Consider optimizing the widget extraction process if possible, perhaps by using a more direct method to access annotations if `pypdf` allows.
11
- # TODO: The `construct_widget` function iterates through `WIDGET_TYPE_PATTERNS` for each widget. If there are many patterns or many widgets, this repeated iteration could be optimized by pre-compiling patterns or using a more efficient lookup mechanism.
12
- # TODO: In `get_widget_key`, the recursive call for `Parent` can lead to deep recursion for deeply nested widgets, potentially impacting performance or hitting recursion limits for extremely complex forms. Consider an iterative approach if deep nesting is common.
13
- # TODO: In `update_widget_keys`, the nested loops iterating through `old_keys`, `out.pages`, and `page.get(Annots, [])` can be very inefficient for large numbers of keys, pages, or annotations. Consider creating a lookup structure for annotations by key to avoid repeated linear scans.
14
- # TODO: In `update_widget_keys`, `PdfReader(stream_to_io(template))` and `out.append(pdf)` involve re-parsing and appending the PDF. For large PDFs, passing `PdfReader` and `PdfWriter` objects directly could reduce overhead.
15
10
 
16
11
  from functools import lru_cache
17
12
  from io import BytesIO
@@ -62,7 +57,7 @@ def build_widgets(
62
57
  key = get_widget_key(widget, use_full_widget_name)
63
58
  _widget = construct_widget(widget, key)
64
59
  if _widget is not None:
65
- _widget.desc = extract_widget_property(
60
+ _widget.__dict__["tooltip"] = extract_widget_property(
66
61
  widget, WIDGET_DESCRIPTION_PATTERNS, None, str
67
62
  )
68
63
 
@@ -12,14 +12,6 @@ It includes functions for:
12
12
  - Generating unique suffixes for internal use.
13
13
  - Enabling Adobe-specific settings in the PDF to ensure proper rendering of form fields.
14
14
  """
15
- # TODO: In `enable_adobe_mode`, `PdfReader(stream_to_io(pdf))` and `writer.append(reader)` involve re-parsing and appending the PDF. For large PDFs, passing `PdfReader` and `PdfWriter` objects directly could reduce overhead.
16
- # TODO: In `remove_all_widgets`, `PdfReader(stream_to_io(pdf))` and iterating through pages to add them to a new writer can be inefficient for large PDFs. Consider if `pypdf` offers a more direct way to remove annotations without reconstructing the entire PDF.
17
- # TODO: In `get_page_streams`, `PdfReader(stream_to_io(pdf))` and then creating a new `PdfWriter` for each page can be very inefficient. It would be more performant to iterate through the pages of a single `PdfReader` and extract their content streams directly if possible, or to use a single `PdfWriter` to extract multiple pages.
18
- # TODO: In `merge_two_pdfs`, the function reads and writes PDFs multiple times (`PdfReader`, `PdfWriter`, `remove_all_widgets`, then another `PdfReader` and `PdfWriter`). This is highly inefficient. The PDF objects should be passed around and modified in-place as much as possible, with a single final write operation.
19
- # TODO: The `merge_two_pdfs` function has a `TODO: refactor duplicate logic with copy_watermark_widgets` comment. This indicates a potential for code duplication and inefficiency. Refactoring this to a shared helper function would improve maintainability and potentially performance.
20
- # TODO: In `find_pattern_match` and `traverse_pattern`, the recursive nature and repeated dictionary lookups (`widget.items()`, `value.get_object()`) can be slow for deeply nested or complex widget structures. Consider optimizing these traversals, perhaps by pre-flattening the widget dictionary or using a more direct access method if `pypdf` allows.
21
- # TODO: In `extract_widget_property`, the loop iterates through `patterns` and calls `traverse_pattern` for each. If `patterns` is long or `traverse_pattern` is expensive, this could be a bottleneck. Consider optimizing the pattern matching or lookup.
22
- # TODO: `generate_unique_suffix` uses `choice` in a loop. While generally fast, for extremely high call volumes, pre-generating a pool of characters or using a faster random string generation method might offer minor improvements.
23
15
 
24
16
  from collections.abc import Callable
25
17
  from functools import lru_cache
@@ -7,13 +7,6 @@ It supports drawing text, lines, and images as watermarks.
7
7
  The module also includes functions to merge these watermarks with the original PDF content
8
8
  and to copy specific widgets from the watermarks to the original PDF.
9
9
  """
10
- # TODO: In `draw_image`, `ImageReader(image_buff)` is created for each image. If the same image is drawn multiple times, consider caching `ImageReader` objects or passing pre-processed image data to avoid redundant processing.
11
- # TODO: In `create_watermarks_and_draw`, `PdfReader(stream_to_io(pdf))` is called, which re-parses the PDF. If this function is called repeatedly for the same PDF, consider passing the `PdfReader` object directly to avoid redundant parsing.
12
- # TODO: In `create_watermarks_and_draw`, the function returns a list of watermarks where only one element is populated. This can be inefficient for memory if there are many pages but only one watermark is created. Consider returning only the created watermark and its page number, and let the caller handle placement.
13
- # TODO: In `merge_watermarks_with_pdf`, `PdfReader(stream_to_io(pdf))` and `PdfReader(stream_to_io(watermarks[i]))` are called in a loop. This leads to repeated parsing of the base PDF and each watermark. It would be more efficient to parse the base PDF once and then merge watermark pages directly into the existing `PdfWriter` object.
14
- # TODO: In `copy_watermark_widgets`, the function reads the PDF and watermarks multiple times. Similar to `merge_watermarks_with_pdf`, optimize by parsing the base PDF and watermarks once and then manipulating the `PdfWriter` object.
15
- # TODO: The `copy_watermark_widgets` function has a `TODO: refactor duplicate logic with merge_two_pdfs` comment. This indicates a potential for code duplication and inefficiency. Refactoring this to a shared helper function would improve maintainability and potentially performance.
16
- # TODO: In `copy_watermark_widgets`, the nested loops iterating through `watermarks`, `watermark_file.pages`, and `page.get(Annots, [])` can be very inefficient for large numbers of watermarks, pages, or annotations. Consider creating a lookup structure for annotations by key to avoid repeated linear scans.
17
10
 
18
11
  from io import BytesIO
19
12
  from typing import List, Union
@@ -12,9 +12,6 @@ Classes:
12
12
  functionality for rendering and manipulation.
13
13
  """
14
14
 
15
- # TODO: In `watermarks`, `PdfReader(stream_to_io(stream))` is called, which re-parses the PDF for each widget. If multiple widgets are being processed, consider passing the `PdfReader` object directly to avoid redundant parsing.
16
- # TODO: In `watermarks`, the list comprehension `[watermark.read() if i == self.page_number - 1 else b"" for i in range(page_count)]` creates a new `BytesIO` object and reads from it for each widget. If many widgets are created, this could be optimized by creating the `BytesIO` object once and passing it around, or by directly returning the watermark bytes and its page number.
17
-
18
15
  from dataclasses import dataclass
19
16
  from inspect import signature
20
17
  from io import BytesIO
@@ -10,8 +10,6 @@ The `RadioWidget` class extends the base `CheckBoxWidget` class to provide
10
10
  specific functionality for interacting with radio button form fields in PDFs.
11
11
  """
12
12
 
13
- # TODO: In `canvas_operations`, `self.acro_form_params.copy()` creates a shallow copy of the dictionary in each iteration of the loop. For a large number of radio buttons, this repeated copying can be inefficient. Consider modifying the dictionary in place and then reverting changes if necessary, or restructuring the data to avoid repeated copying.
14
-
15
13
  from dataclasses import dataclass
16
14
  from typing import List, Optional
17
15
 
@@ -11,10 +11,6 @@ signature form fields in PDFs, including handling their creation, rendering, and
11
11
  integration into the document.
12
12
  """
13
13
 
14
- # TODO: In `watermarks`, `PdfReader(stream_to_io(BEDROCK_PDF))` is called every time the method is invoked. If `BEDROCK_PDF` is static, consider parsing it once and caching the `PdfReader` object to avoid redundant I/O and parsing.
15
- # TODO: In `watermarks`, the list comprehension `[f.read() if i == self.page_number - 1 else b"" for i in range(page_count)]` reads the entire `BytesIO` object `f` multiple times if `page_count` is large. Read `f` once into a variable and then use that variable in the list comprehension.
16
- # TODO: The `input_pdf` is created in `watermarks` but only its page count is used. If the `PdfReader` object is not needed for other operations, consider a lighter way to get the page count or pass the `PdfReader` object from the caller if it's already available.
17
-
18
14
  from dataclasses import dataclass
19
15
  from io import BytesIO
20
16
  from typing import List, Optional
@@ -15,17 +15,6 @@ methods for interacting with its form fields and content. It leverages
15
15
  lower-level modules within the `PyPDFForm` library to handle the
16
16
  underlying PDF manipulation.
17
17
  """
18
- # TODO: The `__add__` method (merging PDFs) involves multiple `self.read()` and `other.read()` calls, leading to redundant PDF parsing. Consider optimizing by passing `PdfReader` objects directly or by performing a single read and then merging.
19
- # TODO: In `_init_helper`, `build_widgets` and `get_all_available_fonts` both call `self.read()`, causing the PDF to be parsed multiple times. Optimize by parsing the PDF once and passing the `PdfReader` object to these functions.
20
- # TODO: The `pages` property's implementation involves `get_page_streams(remove_all_widgets(self.read()))` and `copy_watermark_widgets(each, self.read(), None, i)`. This leads to excessive PDF parsing, widget removal, and copying for each page. Refactor to minimize PDF I/O operations, possibly by working with `pypdf` page objects directly.
21
- # TODO: The `read` method triggers `trigger_widget_hooks` and `enable_adobe_mode`, both of which can involve PDF parsing and writing. Since `read` is called frequently, this can be a performance bottleneck. Consider a more granular dirty-flag system to only apply changes when necessary, or accumulate changes and apply them in a single PDF write operation.
22
- # TODO: The `write` method calls `self.read()`, which in turn triggers all pending operations. This can lead to redundant processing if `read()` has already been called or if multiple `write()` calls are made.
23
- # TODO: In `change_version`, replacing a byte string in the entire PDF stream can be inefficient for very large PDFs. Consider if `pypdf` offers a more direct way to update the PDF version without full stream manipulation.
24
- # TODO: In `generate_coordinate_grid`, `self.read()` is called multiple times, and then `remove_all_widgets`, `generate_coordinate_grid`, and `copy_watermark_widgets` are called, all of which involve PDF parsing and manipulation. Optimize by minimizing PDF I/O and object re-creation.
25
- # TODO: In `fill`, `self.read()` is called, and then `fill` (from `filler.py`), `remove_all_widgets`, and `copy_watermark_widgets` are called. This is a major operation and likely a performance hotspot due to repeated PDF processing. Streamline the PDF modification workflow to reduce redundant parsing and writing.
26
- # TODO: In `create_widget`, `obj.watermarks(self.read())` and `copy_watermark_widgets(self.read(), watermarks, [name], None)` involve reading the PDF multiple times. Optimize by passing the PDF stream or `PdfReader` object more efficiently.
27
- # TODO: The `commit_widget_key_updates` method calls `update_widget_keys`, which involves re-parsing and writing the PDF. For bulk updates, consider a mechanism to apply all key changes in a single PDF modification operation.
28
- # TODO: General: Many methods repeatedly call `self.read()`, which re-parses the PDF. Consider maintaining a persistent `pypdf.PdfReader` and `pypdf.PdfWriter` object internally and only writing to a byte stream when explicitly requested (e.g., by the `read()` or `write()` methods) to avoid redundant I/O and parsing overhead.
29
18
 
30
19
  from __future__ import annotations
31
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 3.5.3
3
+ Version: 3.5.4
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -10,14 +10,14 @@ Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3 :: Only
13
- Classifier: Programming Language :: Python :: 3.9
14
13
  Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
17
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
18
  Classifier: Operating System :: OS Independent
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
- Requires-Python: >=3.9
20
+ Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: cryptography
@@ -17,15 +17,15 @@ classifiers = [
17
17
  "Intended Audience :: Developers",
18
18
  "Programming Language :: Python :: 3",
19
19
  "Programming Language :: Python :: 3 :: Only",
20
- "Programming Language :: Python :: 3.9",
21
20
  "Programming Language :: Python :: 3.10",
22
21
  "Programming Language :: Python :: 3.11",
23
22
  "Programming Language :: Python :: 3.12",
24
23
  "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
25
  "Operating System :: OS Independent",
26
26
  "Topic :: Software Development :: Libraries :: Python Modules",
27
27
  ]
28
- requires-python = ">=3.9"
28
+ requires-python = ">=3.10"
29
29
  dependencies = [
30
30
  "cryptography",
31
31
  "fonttools",
@@ -132,3 +132,8 @@ version = {attr = "PyPDFForm.__version__"}
132
132
 
133
133
  [tool.setuptools.packages.find]
134
134
  include = ["PyPDFForm*"]
135
+
136
+ [tool.pytest.ini_options]
137
+ markers = [
138
+ "posix_only",
139
+ ]
@@ -2,6 +2,8 @@
2
2
 
3
3
  import os
4
4
 
5
+ import pytest
6
+
5
7
  from PyPDFForm import Fields, PdfWrapper
6
8
 
7
9
 
@@ -110,6 +112,7 @@ def test_issue_613(pdf_samples, request):
110
112
  assert obj.read() == expected
111
113
 
112
114
 
115
+ @pytest.mark.posix_only
113
116
  def test_sample_template_library(
114
117
  pdf_samples, image_samples, sample_font_stream, request
115
118
  ):
@@ -25,6 +25,7 @@ def test_create_not_supported_type_not_working(template_stream):
25
25
  assert r
26
26
 
27
27
 
28
+ @pytest.mark.posix_only
28
29
  def test_create_checkbox_default(template_stream, pdf_samples, request):
29
30
  expected_path = os.path.join(
30
31
  pdf_samples, "widget", "test_create_checkbox_default.pdf"
@@ -49,6 +50,7 @@ def test_create_checkbox_default(template_stream, pdf_samples, request):
49
50
  assert obj.read() == expected
50
51
 
51
52
 
53
+ @pytest.mark.posix_only
52
54
  def test_create_checkbox_default_filled(template_stream, pdf_samples, request):
53
55
  expected_path = os.path.join(
54
56
  pdf_samples, "widget", "test_create_checkbox_default_filled.pdf"
@@ -73,6 +75,7 @@ def test_create_checkbox_default_filled(template_stream, pdf_samples, request):
73
75
  assert obj.read() == expected
74
76
 
75
77
 
78
+ @pytest.mark.posix_only
76
79
  def test_create_checkbox_default_filled_flatten(template_stream, pdf_samples, request):
77
80
  expected_path = os.path.join(
78
81
  pdf_samples, "widget", "test_create_checkbox_default_filled_flatten.pdf"
@@ -97,6 +100,7 @@ def test_create_checkbox_default_filled_flatten(template_stream, pdf_samples, re
97
100
  assert obj.read() == expected
98
101
 
99
102
 
103
+ @pytest.mark.posix_only
100
104
  def test_create_checkbox_complex(template_stream, pdf_samples, request):
101
105
  expected_path = os.path.join(
102
106
  pdf_samples, "widget", "test_create_checkbox_complex.pdf"
@@ -128,6 +132,7 @@ def test_create_checkbox_complex(template_stream, pdf_samples, request):
128
132
  assert obj.read() == expected
129
133
 
130
134
 
135
+ @pytest.mark.posix_only
131
136
  def test_create_checkbox_complex_fill(template_stream, pdf_samples, request):
132
137
  expected_path = os.path.join(
133
138
  pdf_samples, "widget", "test_create_checkbox_complex_fill.pdf"
@@ -160,6 +165,7 @@ def test_create_checkbox_complex_fill(template_stream, pdf_samples, request):
160
165
  assert obj.read() == expected
161
166
 
162
167
 
168
+ @pytest.mark.posix_only
163
169
  def test_create_checkbox_complex_fill_flatten(template_stream, pdf_samples, request):
164
170
  expected_path = os.path.join(
165
171
  pdf_samples, "widget", "test_create_checkbox_complex_fill_flatten.pdf"
@@ -192,6 +198,7 @@ def test_create_checkbox_complex_fill_flatten(template_stream, pdf_samples, requ
192
198
  assert obj.read() == expected
193
199
 
194
200
 
201
+ @pytest.mark.posix_only
195
202
  def test_create_checkbox_check_fill(template_stream, pdf_samples, request):
196
203
  expected_path = os.path.join(
197
204
  pdf_samples, "widget", "test_create_checkbox_check_fill.pdf"
@@ -217,6 +224,7 @@ def test_create_checkbox_check_fill(template_stream, pdf_samples, request):
217
224
  assert obj.read() == expected
218
225
 
219
226
 
227
+ @pytest.mark.posix_only
220
228
  def test_create_checkbox_check_fill_flatten(template_stream, pdf_samples, request):
221
229
  expected_path = os.path.join(
222
230
  pdf_samples, "widget", "test_create_checkbox_check_fill_flatten.pdf"
@@ -242,6 +250,7 @@ def test_create_checkbox_check_fill_flatten(template_stream, pdf_samples, reques
242
250
  assert obj.read() == expected
243
251
 
244
252
 
253
+ @pytest.mark.posix_only
245
254
  def test_create_checkbox_circle_fill(template_stream, pdf_samples, request):
246
255
  expected_path = os.path.join(
247
256
  pdf_samples, "widget", "test_create_checkbox_circle_fill.pdf"
@@ -267,6 +276,7 @@ def test_create_checkbox_circle_fill(template_stream, pdf_samples, request):
267
276
  assert obj.read() == expected
268
277
 
269
278
 
279
+ @pytest.mark.posix_only
270
280
  def test_create_checkbox_circle_fill_flatten(template_stream, pdf_samples, request):
271
281
  expected_path = os.path.join(
272
282
  pdf_samples, "widget", "test_create_checkbox_circle_fill_flatten.pdf"
@@ -292,6 +302,7 @@ def test_create_checkbox_circle_fill_flatten(template_stream, pdf_samples, reque
292
302
  assert obj.read() == expected
293
303
 
294
304
 
305
+ @pytest.mark.posix_only
295
306
  def test_create_checkbox_cross_fill(template_stream, pdf_samples, request):
296
307
  expected_path = os.path.join(
297
308
  pdf_samples, "widget", "test_create_checkbox_cross_fill.pdf"
@@ -317,6 +328,7 @@ def test_create_checkbox_cross_fill(template_stream, pdf_samples, request):
317
328
  assert obj.read() == expected
318
329
 
319
330
 
331
+ @pytest.mark.posix_only
320
332
  def test_create_checkbox_cross_fill_flatten(template_stream, pdf_samples, request):
321
333
  expected_path = os.path.join(
322
334
  pdf_samples, "widget", "test_create_checkbox_cross_fill_flatten.pdf"
@@ -342,6 +354,7 @@ def test_create_checkbox_cross_fill_flatten(template_stream, pdf_samples, reques
342
354
  assert obj.read() == expected
343
355
 
344
356
 
357
+ @pytest.mark.posix_only
345
358
  def test_create_text_default(template_stream, pdf_samples, request):
346
359
  expected_path = os.path.join(pdf_samples, "widget", "test_create_text_default.pdf")
347
360
  with open(expected_path, "rb+") as f:
@@ -364,6 +377,7 @@ def test_create_text_default(template_stream, pdf_samples, request):
364
377
  assert obj.read() == expected
365
378
 
366
379
 
380
+ @pytest.mark.posix_only
367
381
  def test_create_text_alpha_bg_color(template_stream, pdf_samples, request):
368
382
  expected_path = os.path.join(
369
383
  pdf_samples, "widget", "test_create_text_alpha_bg_color.pdf"
@@ -389,6 +403,7 @@ def test_create_text_alpha_bg_color(template_stream, pdf_samples, request):
389
403
  assert obj.read() == expected
390
404
 
391
405
 
406
+ @pytest.mark.posix_only
392
407
  def test_create_text_align_center(template_stream, pdf_samples, request):
393
408
  expected_path = os.path.join(
394
409
  pdf_samples, "widget", "test_create_text_align_center.pdf"
@@ -414,6 +429,7 @@ def test_create_text_align_center(template_stream, pdf_samples, request):
414
429
  assert obj.read() == expected
415
430
 
416
431
 
432
+ @pytest.mark.posix_only
417
433
  def test_create_text_align_right(template_stream, pdf_samples, request):
418
434
  expected_path = os.path.join(
419
435
  pdf_samples, "widget", "test_create_text_align_right.pdf"
@@ -439,6 +455,7 @@ def test_create_text_align_right(template_stream, pdf_samples, request):
439
455
  assert obj.read() == expected
440
456
 
441
457
 
458
+ @pytest.mark.posix_only
442
459
  def test_create_text_multiline(template_stream, pdf_samples, request):
443
460
  expected_path = os.path.join(
444
461
  pdf_samples, "widget", "test_create_text_align_multiline.pdf"
@@ -464,6 +481,7 @@ def test_create_text_multiline(template_stream, pdf_samples, request):
464
481
  assert obj.read() == expected
465
482
 
466
483
 
484
+ @pytest.mark.posix_only
467
485
  def test_create_text_default_filled(template_stream, pdf_samples, request):
468
486
  expected_path = os.path.join(
469
487
  pdf_samples, "widget", "test_create_text_default_filled.pdf"
@@ -488,6 +506,7 @@ def test_create_text_default_filled(template_stream, pdf_samples, request):
488
506
  assert obj.read() == expected
489
507
 
490
508
 
509
+ @pytest.mark.posix_only
491
510
  def test_create_text_default_filled_flatten(template_stream, pdf_samples, request):
492
511
  expected_path = os.path.join(
493
512
  pdf_samples, "widget", "test_create_text_default_filled_flatten.pdf"
@@ -512,6 +531,7 @@ def test_create_text_default_filled_flatten(template_stream, pdf_samples, reques
512
531
  assert obj.read() == expected
513
532
 
514
533
 
534
+ @pytest.mark.posix_only
515
535
  def test_create_text_complex(template_stream, pdf_samples, sample_font_stream, request):
516
536
  expected_path = os.path.join(pdf_samples, "widget", "test_create_text_complex.pdf")
517
537
  with open(expected_path, "rb+") as f:
@@ -548,6 +568,7 @@ def test_create_text_complex(template_stream, pdf_samples, sample_font_stream, r
548
568
  assert obj.read() == expected
549
569
 
550
570
 
571
+ @pytest.mark.posix_only
551
572
  def test_create_text_complex_filled(
552
573
  template_stream, pdf_samples, sample_font_stream, request
553
574
  ):
@@ -587,6 +608,7 @@ def test_create_text_complex_filled(
587
608
  assert obj.read() == expected
588
609
 
589
610
 
611
+ @pytest.mark.posix_only
590
612
  def test_create_text_complex_filled_flatten(
591
613
  template_stream, pdf_samples, sample_font_stream, request
592
614
  ):
@@ -626,6 +648,7 @@ def test_create_text_complex_filled_flatten(
626
648
  assert obj.read() == expected
627
649
 
628
650
 
651
+ @pytest.mark.posix_only
629
652
  def test_create_text_comb(template_stream, pdf_samples, request):
630
653
  expected_path = os.path.join(pdf_samples, "widget", "test_create_text_comb.pdf")
631
654
  with open(expected_path, "rb+") as f:
@@ -649,6 +672,7 @@ def test_create_text_comb(template_stream, pdf_samples, request):
649
672
  assert obj.read() == expected
650
673
 
651
674
 
675
+ @pytest.mark.posix_only
652
676
  def test_create_checkbox_persist_old_widgets_fill(
653
677
  template_stream, pdf_samples, request
654
678
  ):
@@ -678,6 +702,7 @@ def test_create_checkbox_persist_old_widgets_fill(
678
702
  assert obj.read() == expected
679
703
 
680
704
 
705
+ @pytest.mark.posix_only
681
706
  def test_create_checkbox_persist_old_widgets_fill_flatten(
682
707
  template_stream, pdf_samples, request
683
708
  ):
@@ -709,6 +734,7 @@ def test_create_checkbox_persist_old_widgets_fill_flatten(
709
734
  assert obj.read() == expected
710
735
 
711
736
 
737
+ @pytest.mark.posix_only
712
738
  def test_create_widget_sejda_fill(sejda_template, pdf_samples, request):
713
739
  expected_path = os.path.join(
714
740
  pdf_samples, "widget", "test_create_widget_sejda_fill.pdf"
@@ -740,6 +766,7 @@ def test_create_widget_sejda_fill(sejda_template, pdf_samples, request):
740
766
  assert obj.read() == expected
741
767
 
742
768
 
769
+ @pytest.mark.posix_only
743
770
  def test_create_widget_sejda_fill_flatten_before(sejda_template, pdf_samples, request):
744
771
  expected_path = os.path.join(
745
772
  pdf_samples, "widget", "test_create_widget_sejda_fill_flatten_before.pdf"
@@ -771,6 +798,7 @@ def test_create_widget_sejda_fill_flatten_before(sejda_template, pdf_samples, re
771
798
  assert obj.read() == expected
772
799
 
773
800
 
801
+ @pytest.mark.posix_only
774
802
  def test_create_widget_sejda_fill_flatten_after(sejda_template, pdf_samples, request):
775
803
  expected_path = os.path.join(
776
804
  pdf_samples, "widget", "test_create_widget_sejda_fill_flatten_after.pdf"
@@ -828,6 +856,7 @@ def test_create_widget_sejda_schema(sejda_template):
828
856
  assert len(schema["properties"]) == len(old_schema["properties"]) + 1
829
857
 
830
858
 
859
+ @pytest.mark.posix_only
831
860
  def test_create_dropdown(template_stream, pdf_samples, sample_font_stream, request):
832
861
  expected_path = os.path.join(pdf_samples, "widget", "test_create_dropdown.pdf")
833
862
  with open(expected_path, "rb+") as f:
@@ -867,6 +896,7 @@ def test_create_dropdown(template_stream, pdf_samples, sample_font_stream, reque
867
896
  assert obj.read() == expected
868
897
 
869
898
 
899
+ @pytest.mark.posix_only
870
900
  def test_create_dropdown_with_export_values(template_stream, pdf_samples, request):
871
901
  expected_path = os.path.join(
872
902
  pdf_samples, "widget", "test_create_dropdown_with_export_values.pdf"
@@ -930,6 +960,7 @@ def test_fill_cmyk_color_flatten(pdf_samples, request):
930
960
  assert obj.read() == expected
931
961
 
932
962
 
963
+ @pytest.mark.posix_only
933
964
  def test_create_radio_default(template_stream, pdf_samples, request):
934
965
  expected_path = os.path.join(pdf_samples, "widget", "test_create_radio_default.pdf")
935
966
  with open(expected_path, "rb+") as f:
@@ -952,6 +983,7 @@ def test_create_radio_default(template_stream, pdf_samples, request):
952
983
  assert obj.read() == expected
953
984
 
954
985
 
986
+ @pytest.mark.posix_only
955
987
  def test_create_radio_default_filled(template_stream, pdf_samples, request):
956
988
  expected_path = os.path.join(
957
989
  pdf_samples, "widget", "test_create_radio_default_filled.pdf"
@@ -976,6 +1008,7 @@ def test_create_radio_default_filled(template_stream, pdf_samples, request):
976
1008
  assert obj.read() == expected
977
1009
 
978
1010
 
1011
+ @pytest.mark.posix_only
979
1012
  def test_create_radio_default_filled_flatten(template_stream, pdf_samples, request):
980
1013
  expected_path = os.path.join(
981
1014
  pdf_samples, "widget", "test_create_radio_default_filled_flatten.pdf"
@@ -1000,6 +1033,7 @@ def test_create_radio_default_filled_flatten(template_stream, pdf_samples, reque
1000
1033
  assert obj.read() == expected
1001
1034
 
1002
1035
 
1036
+ @pytest.mark.posix_only
1003
1037
  def test_create_radio_complex(template_stream, pdf_samples, request):
1004
1038
  expected_path = os.path.join(pdf_samples, "widget", "test_create_radio_complex.pdf")
1005
1039
  with open(expected_path, "rb+") as f:
@@ -1055,6 +1089,7 @@ def test_create_signature_default(template_stream, pdf_samples, request):
1055
1089
  assert obj.read() == expected
1056
1090
 
1057
1091
 
1092
+ @pytest.mark.posix_only
1058
1093
  def test_create_signature_default_filled(
1059
1094
  template_stream, pdf_samples, image_samples, request
1060
1095
  ):
@@ -1086,6 +1121,7 @@ def test_create_signature_default_filled(
1086
1121
  assert obj.read() == expected
1087
1122
 
1088
1123
 
1124
+ @pytest.mark.posix_only
1089
1125
  def test_create_signature_default_filled_flatten(
1090
1126
  template_stream, pdf_samples, image_samples, request
1091
1127
  ):
@@ -1209,6 +1245,7 @@ def test_create_image_default_filled_flatten(
1209
1245
  assert obj.read() == expected
1210
1246
 
1211
1247
 
1248
+ @pytest.mark.posix_only
1212
1249
  def test_create_required_fields(pdf_samples, request):
1213
1250
  expected_path = os.path.join(
1214
1251
  pdf_samples, "widget", "test_create_required_fields.pdf"
@@ -1270,6 +1307,7 @@ def test_create_required_fields(pdf_samples, request):
1270
1307
  assert obj.read() == expected
1271
1308
 
1272
1309
 
1310
+ @pytest.mark.posix_only
1273
1311
  def test_create_not_required_fields(pdf_samples, request):
1274
1312
  expected_path = os.path.join(
1275
1313
  pdf_samples, "widget", "test_create_not_required_fields.pdf"
@@ -1331,6 +1369,7 @@ def test_create_not_required_fields(pdf_samples, request):
1331
1369
  assert obj.read() == expected
1332
1370
 
1333
1371
 
1372
+ @pytest.mark.posix_only
1334
1373
  def test_create_fields_with_tooltips(pdf_samples, request):
1335
1374
  expected_path = os.path.join(
1336
1375
  pdf_samples, "widget", "test_create_fields_with_tooltips.pdf"
@@ -2,6 +2,8 @@
2
2
 
3
3
  import os
4
4
 
5
+ import pytest
6
+
5
7
  from PyPDFForm import PdfWrapper
6
8
 
7
9
 
@@ -270,6 +272,7 @@ def test_dropdown_four_flatten(sample_template_with_dropdown, pdf_samples, reque
270
272
  assert obj.read() == expected
271
273
 
272
274
 
275
+ @pytest.mark.posix_only
273
276
  def test_dropdown_alignment(dropdown_alignment, pdf_samples, request):
274
277
  expected_path = os.path.join(pdf_samples, "dropdown", "test_dropdown_alignment.pdf")
275
278
  with open(expected_path, "rb+") as f:
@@ -286,11 +289,11 @@ def test_dropdown_alignment(dropdown_alignment, pdf_samples, request):
286
289
 
287
290
  expected = f.read()
288
291
 
289
- if os.name != "nt":
290
- assert len(obj.read()) == len(expected)
291
- assert obj.read() == expected
292
+ assert len(obj.read()) == len(expected)
293
+ assert obj.read() == expected
292
294
 
293
295
 
296
+ @pytest.mark.posix_only
294
297
  def test_dropdown_alignment_flatten(dropdown_alignment, pdf_samples, request):
295
298
  expected_path = os.path.join(
296
299
  pdf_samples, "dropdown", "test_dropdown_alignment_flatten.pdf"
@@ -310,11 +313,11 @@ def test_dropdown_alignment_flatten(dropdown_alignment, pdf_samples, request):
310
313
 
311
314
  expected = f.read()
312
315
 
313
- if os.name != "nt":
314
- assert len(obj.read()) == len(expected)
315
- assert obj.read() == expected
316
+ assert len(obj.read()) == len(expected)
317
+ assert obj.read() == expected
316
318
 
317
319
 
320
+ @pytest.mark.posix_only
318
321
  def test_dropdown_alignment_flatten_then_unflatten(
319
322
  dropdown_alignment, pdf_samples, request
320
323
  ):
@@ -337,9 +340,8 @@ def test_dropdown_alignment_flatten_then_unflatten(
337
340
 
338
341
  expected = f.read()
339
342
 
340
- if os.name != "nt":
341
- assert len(obj.read()) == len(expected)
342
- assert obj.read() == expected
343
+ assert len(obj.read()) == len(expected)
344
+ assert obj.read() == expected
343
345
 
344
346
 
345
347
  def test_dropdown_alignment_sejda(dropdown_alignment_sejda, pdf_samples, request):
@@ -99,7 +99,9 @@ def test_pdf_widths_match_computed_font_widths(
99
99
  # Assume that rounding floats to 3 decimal is accurate for most cases
100
100
  assert all(
101
101
  round(pdf_width, 3) == round(computed_width, 3)
102
- for pdf_width, computed_width in zip(pdf_widths_array, computed_widths_array)
102
+ for pdf_width, computed_width in zip(
103
+ pdf_widths_array, computed_widths_array, strict=True
104
+ )
103
105
  )
104
106
 
105
107
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import os
4
4
 
5
+ import pytest
5
6
  from jsonschema import ValidationError, validate
6
7
 
7
8
  from PyPDFForm import PdfWrapper
@@ -81,6 +82,7 @@ def test_register_bad_fonts():
81
82
  assert "foo" not in obj.fonts
82
83
 
83
84
 
85
+ @pytest.mark.posix_only
84
86
  def test_register_global_font_fill(
85
87
  template_stream, pdf_samples, sample_font_stream, data_dict, request
86
88
  ):
@@ -110,6 +112,7 @@ def test_register_global_font_fill(
110
112
  assert obj.read() == expected
111
113
 
112
114
 
115
+ @pytest.mark.posix_only
113
116
  def test_register_global_font_fill_flatten(
114
117
  template_stream, pdf_samples, sample_font_stream, data_dict, request
115
118
  ):
@@ -232,6 +235,7 @@ def test_fill_font_color_red_flatten(template_stream, pdf_samples, data_dict, re
232
235
  assert obj.read() == expected
233
236
 
234
237
 
238
+ @pytest.mark.posix_only
235
239
  def test_fill_with_customized_widgets(
236
240
  template_stream, pdf_samples, sample_font_stream, data_dict, request
237
241
  ):
@@ -261,6 +265,7 @@ def test_fill_with_customized_widgets(
261
265
  assert obj.read() == expected
262
266
 
263
267
 
268
+ @pytest.mark.posix_only
264
269
  def test_fill_with_customized_widgets_flatten(
265
270
  template_stream, pdf_samples, sample_font_stream, data_dict, request
266
271
  ):
@@ -592,6 +597,7 @@ def test_draw_image_on_sejda_template(
592
597
  assert obj.read() == expected
593
598
 
594
599
 
600
+ @pytest.mark.posix_only
595
601
  def test_draw_png_image_on_one_page(
596
602
  template_stream, image_samples, pdf_samples, request
597
603
  ):
@@ -614,6 +620,7 @@ def test_draw_png_image_on_one_page(
614
620
  assert obj.read() == expected
615
621
 
616
622
 
623
+ @pytest.mark.posix_only
617
624
  def test_draw_transparent_png_image_on_one_page(
618
625
  template_stream, image_samples, pdf_samples, request
619
626
  ):
@@ -816,6 +823,7 @@ def test_version(pdf_samples):
816
823
  assert obj.version is None
817
824
 
818
825
 
826
+ @pytest.mark.posix_only
819
827
  def test_fill_font_color(sample_template_with_font_colors, pdf_samples, request):
820
828
  expected_path = os.path.join(pdf_samples, "test_fill_font_color.pdf")
821
829
  with open(expected_path, "rb+") as f:
@@ -833,11 +841,11 @@ def test_fill_font_color(sample_template_with_font_colors, pdf_samples, request)
833
841
 
834
842
  expected = f.read()
835
843
 
836
- if os.name != "nt":
837
- assert len(obj.read()) == len(expected)
838
- assert obj.read() == expected
844
+ assert len(obj.read()) == len(expected)
845
+ assert obj.read() == expected
839
846
 
840
847
 
848
+ @pytest.mark.posix_only
841
849
  def test_fill_font_color_flatten(
842
850
  sample_template_with_font_colors, pdf_samples, request
843
851
  ):
@@ -858,11 +866,11 @@ def test_fill_font_color_flatten(
858
866
 
859
867
  expected = f.read()
860
868
 
861
- if os.name != "nt":
862
- assert len(obj.read()) == len(expected)
863
- assert obj.read() == expected
869
+ assert len(obj.read()) == len(expected)
870
+ assert obj.read() == expected
864
871
 
865
872
 
873
+ @pytest.mark.posix_only
866
874
  def test_fill_complex_fonts(sample_template_with_complex_fonts, pdf_samples, request):
867
875
  expected_path = os.path.join(pdf_samples, "test_fill_complex_fonts.pdf")
868
876
  with open(expected_path, "rb+") as f:
@@ -888,11 +896,11 @@ def test_fill_complex_fonts(sample_template_with_complex_fonts, pdf_samples, req
888
896
 
889
897
  expected = f.read()
890
898
 
891
- if os.name != "nt":
892
- assert len(obj.read()) == len(expected)
893
- assert obj.read() == expected
899
+ assert len(obj.read()) == len(expected)
900
+ assert obj.read() == expected
894
901
 
895
902
 
903
+ @pytest.mark.posix_only
896
904
  def test_fill_complex_fonts_flatten(
897
905
  sample_template_with_complex_fonts, pdf_samples, request
898
906
  ):
@@ -921,9 +929,8 @@ def test_fill_complex_fonts_flatten(
921
929
 
922
930
  expected = f.read()
923
931
 
924
- if os.name != "nt":
925
- assert len(obj.read()) == len(expected)
926
- assert obj.read() == expected
932
+ assert len(obj.read()) == len(expected)
933
+ assert obj.read() == expected
927
934
 
928
935
 
929
936
  def test_pages(template_stream, pdf_samples, request):
@@ -936,6 +943,7 @@ def test_pages(template_stream, pdf_samples, request):
936
943
  assert obj.pages[0].read() == f.read()
937
944
 
938
945
 
946
+ @pytest.mark.posix_only
939
947
  def test_pages_preserve_font(template_stream, pdf_samples, sample_font_stream, request):
940
948
  expected_path = os.path.join(pdf_samples, "pages", "test_pages_preserve_font.pdf")
941
949
  obj = PdfWrapper(template_stream)
@@ -2,6 +2,8 @@
2
2
 
3
3
  import os
4
4
 
5
+ import pytest
6
+
5
7
  from PyPDFForm import PdfWrapper
6
8
 
7
9
 
@@ -87,6 +89,7 @@ def test_paragraph_auto_wrap_flatten(
87
89
  assert obj.read() == expected
88
90
 
89
91
 
92
+ @pytest.mark.posix_only
90
93
  def test_paragraph_auto_font(
91
94
  sample_template_with_paragraph_auto_font, pdf_samples, request
92
95
  ):
@@ -103,11 +106,11 @@ def test_paragraph_auto_font(
103
106
 
104
107
  expected = f.read()
105
108
 
106
- if os.name != "nt":
107
- assert len(obj.read()) == len(expected)
108
- assert obj.read() == expected
109
+ assert len(obj.read()) == len(expected)
110
+ assert obj.read() == expected
109
111
 
110
112
 
113
+ @pytest.mark.posix_only
111
114
  def test_paragraph_auto_font_flatten(
112
115
  sample_template_with_paragraph_auto_font, pdf_samples, request
113
116
  ):
@@ -124,11 +127,11 @@ def test_paragraph_auto_font_flatten(
124
127
 
125
128
  expected = f.read()
126
129
 
127
- if os.name != "nt":
128
- assert len(obj.read()) == len(expected)
129
- assert obj.read() == expected
130
+ assert len(obj.read()) == len(expected)
131
+ assert obj.read() == expected
130
132
 
131
133
 
134
+ @pytest.mark.posix_only
132
135
  def test_paragraph_auto_font_auto_wrap(
133
136
  sample_template_with_paragraph_auto_font, pdf_samples, request
134
137
  ):
@@ -147,11 +150,11 @@ def test_paragraph_auto_font_auto_wrap(
147
150
 
148
151
  expected = f.read()
149
152
 
150
- if os.name != "nt":
151
- assert len(obj.read()) == len(expected)
152
- assert obj.read() == expected
153
+ assert len(obj.read()) == len(expected)
154
+ assert obj.read() == expected
153
155
 
154
156
 
157
+ @pytest.mark.posix_only
155
158
  def test_paragraph_auto_font_auto_wrap_flatten(
156
159
  sample_template_with_paragraph_auto_font, pdf_samples, request
157
160
  ):
@@ -171,9 +174,8 @@ def test_paragraph_auto_font_auto_wrap_flatten(
171
174
 
172
175
  expected = f.read()
173
176
 
174
- if os.name != "nt":
175
- assert len(obj.read()) == len(expected)
176
- assert obj.read() == expected
177
+ assert len(obj.read()) == len(expected)
178
+ assert obj.read() == expected
177
179
 
178
180
 
179
181
  def test_fill_sejda_complex(sejda_template_complex, pdf_samples, request):
@@ -314,6 +316,7 @@ def test_sejda_complex_paragraph_multiple_line_alignment_flatten(
314
316
  assert obj.read() == expected
315
317
 
316
318
 
319
+ @pytest.mark.posix_only
317
320
  def test_paragraph_complex(sample_template_paragraph_complex, pdf_samples, request):
318
321
  expected_path = os.path.join(pdf_samples, "paragraph", "test_paragraph_complex.pdf")
319
322
  with open(expected_path, "rb+") as f:
@@ -333,11 +336,11 @@ def test_paragraph_complex(sample_template_paragraph_complex, pdf_samples, reque
333
336
 
334
337
  expected = f.read()
335
338
 
336
- if os.name != "nt":
337
- assert len(obj.read()) == len(expected)
338
- assert obj.read() == expected
339
+ assert len(obj.read()) == len(expected)
340
+ assert obj.read() == expected
339
341
 
340
342
 
343
+ @pytest.mark.posix_only
341
344
  def test_paragraph_complex_flatten(
342
345
  sample_template_paragraph_complex, pdf_samples, request
343
346
  ):
@@ -362,11 +365,11 @@ def test_paragraph_complex_flatten(
362
365
 
363
366
  expected = f.read()
364
367
 
365
- if os.name != "nt":
366
- assert len(obj.read()) == len(expected)
367
- assert obj.read() == expected
368
+ assert len(obj.read()) == len(expected)
369
+ assert obj.read() == expected
368
370
 
369
371
 
372
+ @pytest.mark.posix_only
370
373
  def test_paragraph_max_length(
371
374
  sample_template_with_paragraph_max_length, pdf_samples, request
372
375
  ):
@@ -385,11 +388,11 @@ def test_paragraph_max_length(
385
388
 
386
389
  expected = f.read()
387
390
 
388
- if os.name != "nt":
389
- assert len(obj.read()) == len(expected)
390
- assert obj.read() == expected
391
+ assert len(obj.read()) == len(expected)
392
+ assert obj.read() == expected
391
393
 
392
394
 
395
+ @pytest.mark.posix_only
393
396
  def test_paragraph_max_length_flatten(
394
397
  sample_template_with_paragraph_max_length, pdf_samples, request
395
398
  ):
@@ -409,6 +412,5 @@ def test_paragraph_max_length_flatten(
409
412
 
410
413
  expected = f.read()
411
414
 
412
- if os.name != "nt":
413
- assert len(obj.read()) == len(expected)
414
- assert obj.read() == expected
415
+ assert len(obj.read()) == len(expected)
416
+ assert obj.read() == expected
@@ -2,9 +2,12 @@
2
2
 
3
3
  import os
4
4
 
5
+ import pytest
6
+
5
7
  from PyPDFForm import PdfWrapper
6
8
 
7
9
 
10
+ @pytest.mark.posix_only
8
11
  def test_fill_signature(pdf_samples, image_samples, request):
9
12
  expected_path = os.path.join(pdf_samples, "signature", "test_fill_signature.pdf")
10
13
  with open(expected_path, "rb+") as f:
@@ -17,9 +20,8 @@ def test_fill_signature(pdf_samples, image_samples, request):
17
20
 
18
21
  expected = f.read()
19
22
 
20
- if os.name != "nt":
21
- assert len(obj.read()) == len(expected)
22
- assert obj.read() == expected
23
+ assert len(obj.read()) == len(expected)
24
+ assert obj.read() == expected
23
25
 
24
26
 
25
27
  def test_signature_schema(pdf_samples):
@@ -40,6 +42,7 @@ def test_signature_sample_value(pdf_samples):
40
42
  )
41
43
 
42
44
 
45
+ @pytest.mark.posix_only
43
46
  def test_fill_signature_overlap(pdf_samples, image_samples, request):
44
47
  expected_path = os.path.join(
45
48
  pdf_samples, "signature", "test_fill_signature_overlap.pdf"
@@ -60,6 +63,7 @@ def test_fill_signature_overlap(pdf_samples, image_samples, request):
60
63
  assert obj.read() == expected
61
64
 
62
65
 
66
+ @pytest.mark.posix_only
63
67
  def test_fill_signature_overlap_not_preserve_aspect_ratio(
64
68
  pdf_samples, image_samples, request
65
69
  ):
@@ -86,6 +90,7 @@ def test_fill_signature_overlap_not_preserve_aspect_ratio(
86
90
  assert obj.read() == expected
87
91
 
88
92
 
93
+ @pytest.mark.posix_only
89
94
  def test_fill_small_icon(pdf_samples, image_samples, request):
90
95
  expected_path = os.path.join(pdf_samples, "signature", "test_fill_small_icon.pdf")
91
96
  with open(expected_path, "rb+") as f:
@@ -105,6 +110,7 @@ def test_fill_small_icon(pdf_samples, image_samples, request):
105
110
  assert obj.read() == expected
106
111
 
107
112
 
113
+ @pytest.mark.posix_only
108
114
  def test_fill_small_icon_not_preserve_aspect_ratio(pdf_samples, image_samples, request):
109
115
  expected_path = os.path.join(
110
116
  pdf_samples, "signature", "test_fill_small_icon_not_preserve_aspect_ratio.pdf"
@@ -2,9 +2,12 @@
2
2
 
3
3
  import os
4
4
 
5
+ import pytest
6
+
5
7
  from PyPDFForm import Fields, PdfWrapper
6
8
 
7
9
 
10
+ @pytest.mark.posix_only
8
11
  def test_register_font_no_form_fields(pdf_samples, sample_font_stream, request):
9
12
  expected_path = os.path.join(
10
13
  pdf_samples, "test_widget_attr_trigger", "test_register_font_no_form_fields.pdf"
@@ -25,6 +28,7 @@ def test_register_font_no_form_fields(pdf_samples, sample_font_stream, request):
25
28
  assert obj.read() == expected
26
29
 
27
30
 
31
+ @pytest.mark.posix_only
28
32
  def test_set_text_field_font(pdf_samples, font_samples, template_stream, request):
29
33
  expected_path = os.path.join(
30
34
  pdf_samples, "test_widget_attr_trigger", "test_set_text_field_font.pdf"
@@ -44,6 +48,7 @@ def test_set_text_field_font(pdf_samples, font_samples, template_stream, request
44
48
  assert obj.read() == expected
45
49
 
46
50
 
51
+ @pytest.mark.posix_only
47
52
  def test_set_text_field_font_sejda(pdf_samples, font_samples, sejda_template, request):
48
53
  expected_path = os.path.join(
49
54
  pdf_samples,
@@ -354,6 +359,7 @@ def test_set_radio_size_sejda(pdf_samples, sejda_template, request):
354
359
  assert obj.read() == expected
355
360
 
356
361
 
362
+ @pytest.mark.posix_only
357
363
  def test_set_dropdown_font(
358
364
  pdf_samples, sample_template_with_dropdown, sample_font_stream, request
359
365
  ):
@@ -374,6 +380,7 @@ def test_set_dropdown_font(
374
380
  assert obj.read() == expected
375
381
 
376
382
 
383
+ @pytest.mark.posix_only
377
384
  def test_set_dropdown_font_sejda(
378
385
  pdf_samples, dropdown_alignment_sejda, sample_font_stream, request
379
386
  ):
@@ -462,3 +469,22 @@ def test_set_dropdown_font_color_sejda(pdf_samples, dropdown_alignment_sejda, re
462
469
 
463
470
  assert len(obj.read()) == len(expected)
464
471
  assert obj.read() == expected
472
+
473
+
474
+ def test_set_text_field_required_sejda(pdf_samples, sejda_template, request):
475
+ expected_path = os.path.join(
476
+ pdf_samples,
477
+ "test_widget_attr_trigger",
478
+ "test_set_text_field_required_sejda.pdf",
479
+ )
480
+ with open(expected_path, "rb+") as f:
481
+ obj = PdfWrapper(sejda_template)
482
+ obj.widgets["buyer_address"].required = True
483
+
484
+ request.config.results["expected_path"] = expected_path
485
+ request.config.results["stream"] = obj.read()
486
+
487
+ expected = f.read()
488
+
489
+ assert len(obj.read()) == len(expected)
490
+ assert obj.read() == expected
File without changes
File without changes
File without changes