PyPDFForm 4.6.2__tar.gz → 4.7.0__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.6.2 → pypdfform-4.7.0}/PKG-INFO +5 -2
  2. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/annotations/__init__.py +3 -0
  4. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/annotations/link.py +12 -4
  5. pypdfform-4.7.0/PyPDFForm/annotations/stamp.py +77 -0
  6. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/constants.py +0 -9
  7. pypdfform-4.6.2/PyPDFForm/ap.py → pypdfform-4.7.0/PyPDFForm/egress.py +21 -13
  8. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/filler.py +2 -1
  9. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/middleware/__init__.py +2 -0
  10. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/middleware/base.py +21 -18
  11. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/template.py +2 -23
  12. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/wrapper.py +8 -10
  13. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm.egg-info/PKG-INFO +5 -2
  14. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm.egg-info/SOURCES.txt +2 -1
  15. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm.egg-info/requires.txt +2 -0
  16. {pypdfform-4.6.2 → pypdfform-4.7.0}/README.md +2 -1
  17. {pypdfform-4.6.2 → pypdfform-4.7.0}/pyproject.toml +2 -0
  18. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_functional.py +129 -0
  19. {pypdfform-4.6.2 → pypdfform-4.7.0}/LICENSE +0 -0
  20. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/adapter.py +0 -0
  21. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/annotations/base.py +0 -0
  22. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/annotations/text.py +0 -0
  23. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/annotations/text_markup.py +0 -0
  24. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/assets/__init__.py +0 -0
  25. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/assets/bedrock.py +0 -0
  26. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/assets/blank.py +0 -0
  27. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/coordinate.py +0 -0
  28. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/deprecation.py +0 -0
  29. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/font.py +0 -0
  30. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/hooks.py +0 -0
  31. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/image.py +0 -0
  32. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/middleware/checkbox.py +1 -1
  33. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/middleware/dropdown.py +2 -2
  34. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/middleware/image.py +0 -0
  35. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/middleware/radio.py +0 -0
  36. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/middleware/signature.py +0 -0
  37. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/middleware/text.py +1 -1
  38. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/patterns.py +0 -0
  39. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/raw/__init__.py +0 -0
  40. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/raw/circle.py +0 -0
  41. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/raw/ellipse.py +0 -0
  42. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/raw/image.py +0 -0
  43. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/raw/line.py +0 -0
  44. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/raw/rect.py +0 -0
  45. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/raw/text.py +0 -0
  46. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/types.py +0 -0
  47. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/utils.py +0 -0
  48. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/watermark.py +0 -0
  49. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/widgets/__init__.py +0 -0
  50. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/widgets/base.py +0 -0
  51. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/widgets/checkbox.py +0 -0
  52. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/widgets/dropdown.py +0 -0
  53. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/widgets/image.py +0 -0
  54. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/widgets/radio.py +0 -0
  55. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/widgets/signature.py +0 -0
  56. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm/widgets/text.py +0 -0
  57. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  58. {pypdfform-4.6.2 → pypdfform-4.7.0}/PyPDFForm.egg-info/top_level.txt +0 -0
  59. {pypdfform-4.6.2 → pypdfform-4.7.0}/setup.cfg +0 -0
  60. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_bulk_create_fields.py +0 -0
  61. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_create_widget.py +0 -0
  62. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_draw_elements.py +0 -0
  63. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_dropdown.py +0 -0
  64. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_extract_middleware_attributes.py +0 -0
  65. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_fill_max_length_text_field.py +0 -0
  66. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_font_widths.py +0 -0
  67. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_generate_appearance_streams.py +0 -0
  68. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_js.py +0 -0
  69. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_need_appearances.py +0 -0
  70. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_paragraph.py +0 -0
  71. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_signature.py +0 -0
  72. {pypdfform-4.6.2 → pypdfform-4.7.0}/tests/test_use_full_widget_name.py +0 -0
  73. {pypdfform-4.6.2 → pypdfform-4.7.0}/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.6.2
3
+ Version: 4.7.0
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -31,6 +31,8 @@ Requires-Dist: black<27.0.0,>=25.11.0; extra == "dev"
31
31
  Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
32
32
  Requires-Dist: isort<9.0.0,>=7.0.0; extra == "dev"
33
33
  Requires-Dist: jsonschema<5.0.0,>=4.25.1; extra == "dev"
34
+ Requires-Dist: mike<3.0.0,>=2.1.3; extra == "dev"
35
+ Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "dev"
34
36
  Requires-Dist: mkdocs-material<10.0.0,>=9.7.0; extra == "dev"
35
37
  Requires-Dist: pudb<2026.0.0,>=2025.1.3; extra == "dev"
36
38
  Requires-Dist: pylint<5.0.0,>=4.0.3; extra == "dev"
@@ -48,7 +50,7 @@ Dynamic: license-file
48
50
  <a href="https://pypi.org/project/PyPDFForm/"><img src="https://img.shields.io/pypi/v/pypdfform?label=version&color=magenta"></a>
49
51
  <a href="https://chinapandaman.github.io/PyPDFForm/"><img src="https://img.shields.io/github/v/release/chinapandaman/pypdfform?label=docs&color=cyan"></a>
50
52
  <a href="https://github.com/chinapandaman/PyPDFForm/actions/workflows/python-package.yml"><img src="https://img.shields.io/badge/coverage-100%25-green"></a>
51
- <a href="https://github.com/chinapandaman/PyPDFForm/raw/master/LICENSE"><img src="https://img.shields.io/github/license/chinapandaman/pypdfform?label=license&color=orange"></a>
53
+ <a href="https://github.com/chinapandaman/PyPDFForm/blob/master/LICENSE"><img src="https://img.shields.io/github/license/chinapandaman/pypdfform?label=license&color=orange"></a>
52
54
  <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/pypi/pyversions/pypdfform?label=python&color=gold"></a>
53
55
  <a href="https://pepy.tech/projects/pypdfform"><img src="https://static.pepy.tech/badge/pypdfform/month"></a>
54
56
  </p>
@@ -102,6 +104,7 @@ The official documentation can be found on [the GitHub page](https://chinapandam
102
104
 
103
105
  ## Other Resources
104
106
 
107
+ <!-- TODO: remove WIP when finish recording -->
105
108
  * [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
106
109
  * [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
107
110
 
@@ -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__ = "4.6.2"
23
+ __version__ = "4.7.0"
24
24
 
25
25
  from .annotations import Annotations
26
26
  from .assets.blank import BlankPage
@@ -12,6 +12,7 @@ annotations, facilitating their creation and manipulation within PDF documents.
12
12
  from dataclasses import dataclass
13
13
 
14
14
  from .link import LinkAnnotation
15
+ from .stamp import RubberStampAnnotation
15
16
  from .text import TextAnnotation
16
17
  from .text_markup import (HighlightAnnotation, SquigglyAnnotation,
17
18
  StrikeOutAnnotation, UnderlineAnnotation)
@@ -23,6 +24,7 @@ AnnotationTypes = (
23
24
  | UnderlineAnnotation
24
25
  | SquigglyAnnotation
25
26
  | StrikeOutAnnotation
27
+ | RubberStampAnnotation
26
28
  )
27
29
 
28
30
 
@@ -41,3 +43,4 @@ class Annotations:
41
43
  UnderlineAnnotation = UnderlineAnnotation
42
44
  SquigglyAnnotation = SquigglyAnnotation
43
45
  StrikeOutAnnotation = StrikeOutAnnotation
46
+ RubberStampAnnotation = RubberStampAnnotation
@@ -4,7 +4,7 @@
4
4
  This module defines the class for link annotations in PyPDFForm.
5
5
 
6
6
  It provides a structure for representing and interacting with PDF link
7
- annotations, which allow users to click and navigate to a URI.
7
+ annotations, which allow users to click and navigate to a URI or an internal page.
8
8
 
9
9
  Classes:
10
10
  - `LinkAnnotation`: A dataclass representing the properties of a PDF link annotation.
@@ -13,7 +13,8 @@ Classes:
13
13
  from dataclasses import dataclass
14
14
  from typing import Optional
15
15
 
16
- from pypdf.generic import DictionaryObject, NameObject, TextStringObject
16
+ from pypdf.generic import (ArrayObject, DictionaryObject, NameObject,
17
+ NumberObject, TextStringObject)
17
18
 
18
19
  from ..constants import A, S
19
20
  from .base import Annotation
@@ -25,21 +26,24 @@ class LinkAnnotation(Annotation):
25
26
  A dataclass representing the properties of a PDF link annotation.
26
27
 
27
28
  This class extends the `Annotation` base class to specifically handle
28
- link annotations, including the target URI.
29
+ link annotations, including the target URI or an internal page.
29
30
 
30
31
  Attributes:
31
32
  uri (str): The URI that the link annotation points to. Defaults to None.
33
+ page (int): The 1-based page number that the link annotation points to. Defaults to None.
32
34
  """
33
35
 
34
36
  _annotation_type: str = "/Link"
35
37
 
36
38
  uri: Optional[str] = None
39
+ page: Optional[int] = None
37
40
 
38
41
  def get_specific_properties(self) -> dict:
39
42
  """
40
43
  Gets properties specific to the link annotation.
41
44
 
42
- This method extends the base properties with the URI action.
45
+ This method extends the base properties with either a URI action
46
+ or a destination for an internal page link.
43
47
 
44
48
  Returns:
45
49
  dict: A dictionary of PDF properties specific to the link annotation.
@@ -52,5 +56,9 @@ class LinkAnnotation(Annotation):
52
56
  NameObject("/URI"): TextStringObject(self.uri),
53
57
  }
54
58
  )
59
+ elif self.page is not None:
60
+ result[NameObject("/Dest")] = ArrayObject(
61
+ [NumberObject(self.page - 1), NameObject("/Fit")]
62
+ )
55
63
 
56
64
  return result
@@ -0,0 +1,77 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module defines the `RubberStampAnnotation` class, which is used to represent
4
+ a rubber stamp annotation in a PDF document.
5
+
6
+ The `RubberStampAnnotation` class encapsulates the properties of a rubber stamp
7
+ annotation, such as its location, dimensions, content, and the stamp name.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Optional
12
+
13
+ from pypdf.generic import NameObject
14
+
15
+ from .base import Annotation
16
+
17
+
18
+ @dataclass
19
+ class RubberStampAnnotation(Annotation):
20
+ """
21
+ Represents a rubber stamp annotation in a PDF document.
22
+
23
+ This dataclass extends the base `Annotation` class and defines the specific
24
+ attributes and metadata associated with a rubber stamp annotation.
25
+
26
+ Attributes:
27
+ _annotation_type (str): The PDF internal type of the annotation, which is "/Stamp".
28
+ approved (str): The identifier for an "Approved" stamp.
29
+ experimental (str): The identifier for an "Experimental" stamp.
30
+ not_approved (str): The identifier for a "NotApproved" stamp.
31
+ as_is (str): The identifier for an "AsIs" stamp.
32
+ expired (str): The identifier for an "Expired" stamp.
33
+ not_for_public_release (str): The identifier for a "NotForPublicRelease" stamp.
34
+ confidential (str): The identifier for a "Confidential" stamp.
35
+ final (str): The identifier for a "Final" stamp.
36
+ sold (str): The identifier for a "Sold" stamp.
37
+ departmental (str): The identifier for a "Departmental" stamp.
38
+ for_comment (str): The identifier for a "ForComment" stamp.
39
+ top_secret (str): The identifier for a "TopSecret" stamp.
40
+ draft (str): The identifier for a "Draft" stamp.
41
+ for_public_release (str): The identifier for a "ForPublicRelease" stamp.
42
+ name (Optional[str]): The name of the stamp to be used.
43
+ """
44
+
45
+ _annotation_type: str = "/Stamp"
46
+
47
+ approved = "/Approved"
48
+ experimental = "/Experimental"
49
+ not_approved = "/NotApproved"
50
+ as_is = "/AsIs"
51
+ expired = "/Expired"
52
+ not_for_public_release = "/NotForPublicRelease"
53
+ confidential = "/Confidential"
54
+ final = "/Final"
55
+ sold = "/Sold"
56
+ departmental = "/Departmental"
57
+ for_comment = "/ForComment"
58
+ top_secret = "/TopSecret"
59
+ draft = "/Draft"
60
+ for_public_release = "/ForPublicRelease"
61
+
62
+ name: Optional[str] = None
63
+
64
+ def get_specific_properties(self) -> dict:
65
+ """
66
+ Gets properties specific to the rubber stamp annotation.
67
+
68
+ This method extends the base properties with the stamp name if it is provided.
69
+
70
+ Returns:
71
+ dict: A dictionary of PDF properties specific to the rubber stamp annotation.
72
+ """
73
+ result = super().get_specific_properties()
74
+ if self.name is not None:
75
+ result[NameObject("/Name")] = NameObject(self.name)
76
+
77
+ return result
@@ -15,13 +15,6 @@ Using constants improves code readability and maintainability by providing
15
15
  meaningful names for frequently used values and reducing the risk of typos.
16
16
  """
17
17
 
18
- from .middleware.checkbox import Checkbox
19
- from .middleware.dropdown import Dropdown
20
- from .middleware.image import Image
21
- from .middleware.radio import Radio
22
- from .middleware.signature import Signature
23
- from .middleware.text import Text
24
-
25
18
  VERSION_IDENTIFIERS = [
26
19
  b"%PDF-1.0",
27
20
  b"%PDF-1.1",
@@ -35,8 +28,6 @@ VERSION_IDENTIFIERS = [
35
28
  ]
36
29
  VERSION_IDENTIFIER_PREFIX = "%PDF-".encode("utf-8")
37
30
 
38
- WIDGET_TYPES = Text | Checkbox | Radio | Dropdown | Signature | Image
39
-
40
31
  DEPRECATION_NOTICE = "{} will be deprecated soon. Use {} instead."
41
32
 
42
33
  Title = "/Title"
@@ -1,10 +1,13 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- A module for handling PDF appearance streams.
4
-
5
- This module provides functionality to manage appearance streams in PDF forms,
6
- which are necessary for form fields to display correctly after being filled.
7
- It uses both pypdf and pikepdf for manipulation.
3
+ A module for egress functions.
4
+
5
+ This module provides functionalities that prepare the final PDF for output (egress),
6
+ ensuring that it is properly formatted and ready for the end-user. This includes
7
+ managing appearance streams (so form fields display correctly after being filled),
8
+ handling the /NeedAppearances flag, and preserving or updating document-level
9
+ properties like metadata, title, and OpenAction scripts. These functions are typically
10
+ called right before the final PDF byte stream is returned by the wrapper module.
8
11
  """
9
12
 
10
13
  from functools import lru_cache
@@ -66,18 +69,20 @@ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) ->
66
69
  return result
67
70
 
68
71
 
69
- @lru_cache
70
- def preserve_pdf_properties(pdf: bytes, title: str, script: str) -> bytes:
72
+ def preserve_pdf_properties(
73
+ pdf: bytes, title: str, script: str, metadata: dict
74
+ ) -> bytes:
71
75
  """
72
- Preserves and updates PDF properties such as title and OpenAction scripts.
76
+ Preserves and updates PDF properties such as metadata, title, and OpenAction scripts.
73
77
 
74
- This function allows setting or updating the PDF's title in its metadata and
78
+ This function allows setting or updating the PDF's title and metadata, and
75
79
  attaching a JavaScript script that executes when the PDF is opened.
76
80
 
77
81
  Args:
78
82
  pdf (bytes): The PDF file content as a bytes stream.
79
83
  title (str): The title to be set in the PDF metadata.
80
84
  script (str): JavaScript code to be executed when the PDF is opened.
85
+ metadata (dict): The original metadata to preserve.
81
86
 
82
87
  Returns:
83
88
  bytes: The modified PDF content as a bytes stream.
@@ -86,11 +91,14 @@ def preserve_pdf_properties(pdf: bytes, title: str, script: str) -> bytes:
86
91
  writer = PdfWriter()
87
92
  writer.append(reader)
88
93
 
89
- if title:
90
- metadata = reader.metadata or {}
91
- metadata[NameObject(Title)] = TextStringObject(title)
94
+ if title or metadata:
95
+ _metadata = reader.metadata or {}
96
+ if metadata:
97
+ _metadata.update(metadata)
98
+ if title:
99
+ _metadata[NameObject(Title)] = TextStringObject(title)
92
100
 
93
- writer.add_metadata(metadata)
101
+ writer.add_metadata(_metadata)
94
102
 
95
103
  if script:
96
104
  open_action = DictionaryObject()
@@ -14,9 +14,10 @@ from typing import Dict, cast
14
14
  from pypdf import PdfReader, PdfWriter
15
15
  from pypdf.generic import DictionaryObject
16
16
 
17
- from .constants import WIDGET_TYPES, Annots
17
+ from .constants import Annots
18
18
  from .hooks import flatten_field
19
19
  from .image import get_draw_image_resolutions, get_image_dimensions
20
+ from .middleware import WIDGET_TYPES
20
21
  from .middleware.checkbox import Checkbox
21
22
  from .middleware.dropdown import Dropdown
22
23
  from .middleware.image import Image
@@ -17,6 +17,8 @@ from .radio import Radio
17
17
  from .signature import Signature
18
18
  from .text import Text
19
19
 
20
+ WIDGET_TYPES = Text | Checkbox | Radio | Dropdown | Signature | Image
21
+
20
22
 
21
23
  @dataclass
22
24
  class Widgets:
@@ -21,23 +21,6 @@ class Widget:
21
21
  as name, value, and schema definition.
22
22
  """
23
23
 
24
- SET_ATTR_TRIGGER_HOOK_MAP = {
25
- "x": "update_field_x",
26
- "y": "update_field_y",
27
- "width": "update_field_width",
28
- "height": "update_field_height",
29
- "readonly": "flatten_field",
30
- "required": "update_field_required",
31
- "hidden": "update_field_hidden",
32
- "tooltip": "update_field_tooltip",
33
- "on_hovered_over_javascript": "update_field_on_hovered_over_javascript",
34
- "on_hovered_off_javascript": "update_field_on_hovered_off_javascript",
35
- "on_mouse_pressed_javascript": "update_field_on_mouse_pressed_javascript",
36
- "on_mouse_released_javascript": "update_field_on_mouse_released_javascript",
37
- "on_focused_javascript": "update_field_on_focused_javascript",
38
- "on_blurred_javascript": "update_field_on_blurred_javascript",
39
- }
40
-
41
24
  def __init__(
42
25
  self,
43
26
  name: str,
@@ -51,6 +34,22 @@ class Widget:
51
34
  value (Any): The initial value of the widget. Defaults to None.
52
35
  """
53
36
  super().__init__()
37
+ self.SET_ATTR_TRIGGER_HOOK_MAP = {
38
+ "x": "update_field_x",
39
+ "y": "update_field_y",
40
+ "width": "update_field_width",
41
+ "height": "update_field_height",
42
+ "readonly": "flatten_field",
43
+ "required": "update_field_required",
44
+ "hidden": "update_field_hidden",
45
+ "tooltip": "update_field_tooltip",
46
+ "on_hovered_over_javascript": "update_field_on_hovered_over_javascript",
47
+ "on_hovered_off_javascript": "update_field_on_hovered_off_javascript",
48
+ "on_mouse_pressed_javascript": "update_field_on_mouse_pressed_javascript",
49
+ "on_mouse_released_javascript": "update_field_on_mouse_released_javascript",
50
+ "on_focused_javascript": "update_field_on_focused_javascript",
51
+ "on_blurred_javascript": "update_field_on_blurred_javascript",
52
+ }
54
53
  self.attr_set_tracker = {}
55
54
 
56
55
  self._name = name
@@ -88,7 +87,11 @@ class Widget:
88
87
  name (str): The name of the attribute.
89
88
  value (Any): The value of the attribute.
90
89
  """
91
- if name in self.SET_ATTR_TRIGGER_HOOK_MAP and value is not None:
90
+ if (
91
+ hasattr(self, "SET_ATTR_TRIGGER_HOOK_MAP")
92
+ and name in self.SET_ATTR_TRIGGER_HOOK_MAP
93
+ and value is not None
94
+ ):
92
95
  self.hooks_to_trigger.append((self.SET_ATTR_TRIGGER_HOOK_MAP[name], value))
93
96
 
94
97
  if (
@@ -16,8 +16,8 @@ from pypdf import PdfReader, PdfWriter
16
16
  from pypdf.generic import ArrayObject, DictionaryObject, NameObject
17
17
 
18
18
  from .annotations import AnnotationTypes
19
- from .constants import (COMB, MULTILINE, READ_ONLY, REQUIRED, WIDGET_TYPES,
20
- Annots)
19
+ from .constants import COMB, MULTILINE, READ_ONLY, REQUIRED, Annots
20
+ from .middleware import WIDGET_TYPES
21
21
  from .middleware.checkbox import Checkbox
22
22
  from .middleware.dropdown import Dropdown
23
23
  from .middleware.radio import Radio
@@ -49,27 +49,6 @@ def get_metadata(pdf: bytes) -> dict:
49
49
  return reader.metadata or {}
50
50
 
51
51
 
52
- def set_metadata(pdf: bytes, metadata: dict) -> bytes:
53
- """
54
- Sets the metadata of a PDF.
55
-
56
- Args:
57
- pdf (bytes): The PDF stream to set metadata for.
58
- metadata (dict): A dictionary containing the metadata to be set.
59
-
60
- Returns:
61
- bytes: The updated PDF stream with the new metadata.
62
- """
63
- reader = PdfReader(stream_to_io(pdf))
64
- writer = PdfWriter(clone_from=reader)
65
- writer.add_metadata(metadata)
66
-
67
- with BytesIO() as f:
68
- writer.write(f)
69
- f.seek(0)
70
- return f.read()
71
-
72
-
73
52
  def build_widgets(
74
53
  pdf_stream: bytes,
75
54
  use_full_widget_name: bool,
@@ -27,9 +27,9 @@ from typing import (TYPE_CHECKING, BinaryIO, Dict, Optional, Sequence, TextIO,
27
27
 
28
28
  from .adapter import (fp_or_f_obj_or_f_content_to_content,
29
29
  fp_or_f_obj_or_stream_to_stream)
30
- from .ap import appearance_streams_handler, preserve_pdf_properties
31
30
  from .constants import VERSION_IDENTIFIER_PREFIX, VERSION_IDENTIFIERS
32
31
  from .coordinate import generate_coordinate_grid
32
+ from .egress import appearance_streams_handler, preserve_pdf_properties
33
33
  from .filler import fill
34
34
  from .font import (get_all_available_fonts, register_font,
35
35
  register_font_acroform)
@@ -38,7 +38,7 @@ from .middleware.dropdown import Dropdown
38
38
  from .middleware.signature import Signature
39
39
  from .middleware.text import Text
40
40
  from .template import (build_widgets, create_annotations, get_metadata,
41
- set_metadata, update_widget_keys)
41
+ update_widget_keys)
42
42
  from .types import PdfArray
43
43
  from .utils import (generate_unique_suffix, get_page_streams, merge_pdfs,
44
44
  remove_all_widgets)
@@ -362,9 +362,8 @@ class PdfWrapper:
362
362
  2. If `need_appearances` is enabled, it handles appearance streams and the
363
363
  `/NeedAppearances` flag, which may include removing XFA and explicitly
364
364
  generating appearance streams.
365
- 3. If `preserve_metadata` is enabled, it preserves the original metadata of the PDF.
366
- 4. If a title or on-open JavaScript is set, it updates the PDF properties
367
- accordingly.
365
+ 3. If `preserve_metadata`, title, or on-open JavaScript are set, it preserves
366
+ or updates the corresponding PDF properties accordingly.
368
367
 
369
368
  Returns:
370
369
  bytes: The processed PDF document content as a byte string.
@@ -376,15 +375,14 @@ class PdfWrapper:
376
375
  result, getattr(self, "generate_appearance_streams")
377
376
  ) # cached
378
377
 
379
- if getattr(self, "preserve_metadata"):
380
- # TODO: refactor with preserve_pdf_properties
381
- result = set_metadata(result, self._metadata)
382
-
383
- if any([self.title, self.on_open_javascript]):
378
+ if any(
379
+ [getattr(self, "preserve_metadata"), self.title, self.on_open_javascript]
380
+ ):
384
381
  result = preserve_pdf_properties(
385
382
  result,
386
383
  self.title,
387
384
  self.on_open_javascript,
385
+ self._metadata if getattr(self, "preserve_metadata") else None,
388
386
  )
389
387
 
390
388
  return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 4.6.2
3
+ Version: 4.7.0
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -31,6 +31,8 @@ Requires-Dist: black<27.0.0,>=25.11.0; extra == "dev"
31
31
  Requires-Dist: coverage<8.0.0,>=7.12.0; extra == "dev"
32
32
  Requires-Dist: isort<9.0.0,>=7.0.0; extra == "dev"
33
33
  Requires-Dist: jsonschema<5.0.0,>=4.25.1; extra == "dev"
34
+ Requires-Dist: mike<3.0.0,>=2.1.3; extra == "dev"
35
+ Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "dev"
34
36
  Requires-Dist: mkdocs-material<10.0.0,>=9.7.0; extra == "dev"
35
37
  Requires-Dist: pudb<2026.0.0,>=2025.1.3; extra == "dev"
36
38
  Requires-Dist: pylint<5.0.0,>=4.0.3; extra == "dev"
@@ -48,7 +50,7 @@ Dynamic: license-file
48
50
  <a href="https://pypi.org/project/PyPDFForm/"><img src="https://img.shields.io/pypi/v/pypdfform?label=version&color=magenta"></a>
49
51
  <a href="https://chinapandaman.github.io/PyPDFForm/"><img src="https://img.shields.io/github/v/release/chinapandaman/pypdfform?label=docs&color=cyan"></a>
50
52
  <a href="https://github.com/chinapandaman/PyPDFForm/actions/workflows/python-package.yml"><img src="https://img.shields.io/badge/coverage-100%25-green"></a>
51
- <a href="https://github.com/chinapandaman/PyPDFForm/raw/master/LICENSE"><img src="https://img.shields.io/github/license/chinapandaman/pypdfform?label=license&color=orange"></a>
53
+ <a href="https://github.com/chinapandaman/PyPDFForm/blob/master/LICENSE"><img src="https://img.shields.io/github/license/chinapandaman/pypdfform?label=license&color=orange"></a>
52
54
  <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/pypi/pyversions/pypdfform?label=python&color=gold"></a>
53
55
  <a href="https://pepy.tech/projects/pypdfform"><img src="https://static.pepy.tech/badge/pypdfform/month"></a>
54
56
  </p>
@@ -102,6 +104,7 @@ The official documentation can be found on [the GitHub page](https://chinapandam
102
104
 
103
105
  ## Other Resources
104
106
 
107
+ <!-- TODO: remove WIP when finish recording -->
105
108
  * [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
106
109
  * [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
107
110
 
@@ -3,10 +3,10 @@ README.md
3
3
  pyproject.toml
4
4
  PyPDFForm/__init__.py
5
5
  PyPDFForm/adapter.py
6
- PyPDFForm/ap.py
7
6
  PyPDFForm/constants.py
8
7
  PyPDFForm/coordinate.py
9
8
  PyPDFForm/deprecation.py
9
+ PyPDFForm/egress.py
10
10
  PyPDFForm/filler.py
11
11
  PyPDFForm/font.py
12
12
  PyPDFForm/hooks.py
@@ -25,6 +25,7 @@ PyPDFForm.egg-info/top_level.txt
25
25
  PyPDFForm/annotations/__init__.py
26
26
  PyPDFForm/annotations/base.py
27
27
  PyPDFForm/annotations/link.py
28
+ PyPDFForm/annotations/stamp.py
28
29
  PyPDFForm/annotations/text.py
29
30
  PyPDFForm/annotations/text_markup.py
30
31
  PyPDFForm/assets/__init__.py
@@ -10,6 +10,8 @@ black<27.0.0,>=25.11.0
10
10
  coverage<8.0.0,>=7.12.0
11
11
  isort<9.0.0,>=7.0.0
12
12
  jsonschema<5.0.0,>=4.25.1
13
+ mike<3.0.0,>=2.1.3
14
+ mkdocs<2.0.0,>=1.6.1
13
15
  mkdocs-material<10.0.0,>=9.7.0
14
16
  pudb<2026.0.0,>=2025.1.3
15
17
  pylint<5.0.0,>=4.0.3
@@ -6,7 +6,7 @@
6
6
  <a href="https://pypi.org/project/PyPDFForm/"><img src="https://img.shields.io/pypi/v/pypdfform?label=version&color=magenta"></a>
7
7
  <a href="https://chinapandaman.github.io/PyPDFForm/"><img src="https://img.shields.io/github/v/release/chinapandaman/pypdfform?label=docs&color=cyan"></a>
8
8
  <a href="https://github.com/chinapandaman/PyPDFForm/actions/workflows/python-package.yml"><img src="https://img.shields.io/badge/coverage-100%25-green"></a>
9
- <a href="https://github.com/chinapandaman/PyPDFForm/raw/master/LICENSE"><img src="https://img.shields.io/github/license/chinapandaman/pypdfform?label=license&color=orange"></a>
9
+ <a href="https://github.com/chinapandaman/PyPDFForm/blob/master/LICENSE"><img src="https://img.shields.io/github/license/chinapandaman/pypdfform?label=license&color=orange"></a>
10
10
  <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/pypi/pyversions/pypdfform?label=python&color=gold"></a>
11
11
  <a href="https://pepy.tech/projects/pypdfform"><img src="https://static.pepy.tech/badge/pypdfform/month"></a>
12
12
  </p>
@@ -60,6 +60,7 @@ The official documentation can be found on [the GitHub page](https://chinapandam
60
60
 
61
61
  ## Other Resources
62
62
 
63
+ <!-- TODO: remove WIP when finish recording -->
63
64
  * [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
64
65
  * [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
65
66
 
@@ -45,6 +45,8 @@ dev = [
45
45
  "coverage>=7.12.0,<8.0.0",
46
46
  "isort>=7.0.0,<9.0.0",
47
47
  "jsonschema>=4.25.1,<5.0.0",
48
+ "mike>=2.1.3,<3.0.0",
49
+ "mkdocs>=1.6.1,<2.0.0",
48
50
  "mkdocs-material>=9.7.0,<10.0.0",
49
51
  "pudb>=2025.1.3,<2026.0.0",
50
52
  "pylint>=4.0.3,<5.0.0",
@@ -884,3 +884,132 @@ def test_annotate_no_annotations(pdf_samples, request):
884
884
 
885
885
  assert len(obj.read()) == len(expected)
886
886
  assert obj.read() == expected
887
+
888
+
889
+ def test_rubber_stamp_annotation(template_stream, pdf_samples, request):
890
+ expected_path = os.path.join(pdf_samples, "test_rubber_stamp_annotation.pdf")
891
+ with open(expected_path, "rb+") as f:
892
+ obj = PdfWrapper(template_stream).annotate(
893
+ [
894
+ Annotations.RubberStampAnnotation(
895
+ 1,
896
+ 0,
897
+ 400,
898
+ width=50,
899
+ height=50,
900
+ name=Annotations.RubberStampAnnotation.approved,
901
+ ),
902
+ Annotations.RubberStampAnnotation(
903
+ 1,
904
+ 100,
905
+ 400,
906
+ width=50,
907
+ height=50,
908
+ name=Annotations.RubberStampAnnotation.experimental,
909
+ ),
910
+ Annotations.RubberStampAnnotation(
911
+ 1,
912
+ 200,
913
+ 400,
914
+ width=50,
915
+ height=50,
916
+ name=Annotations.RubberStampAnnotation.not_approved,
917
+ ),
918
+ Annotations.RubberStampAnnotation(
919
+ 1,
920
+ 300,
921
+ 400,
922
+ width=50,
923
+ height=50,
924
+ name=Annotations.RubberStampAnnotation.as_is,
925
+ ),
926
+ Annotations.RubberStampAnnotation(
927
+ 1,
928
+ 400,
929
+ 400,
930
+ width=50,
931
+ height=50,
932
+ name=Annotations.RubberStampAnnotation.expired,
933
+ ),
934
+ Annotations.RubberStampAnnotation(
935
+ 1,
936
+ 0,
937
+ 500,
938
+ width=50,
939
+ height=50,
940
+ name=Annotations.RubberStampAnnotation.not_for_public_release,
941
+ ),
942
+ Annotations.RubberStampAnnotation(
943
+ 1,
944
+ 100,
945
+ 500,
946
+ width=50,
947
+ height=50,
948
+ name=Annotations.RubberStampAnnotation.confidential,
949
+ ),
950
+ Annotations.RubberStampAnnotation(
951
+ 1,
952
+ 200,
953
+ 500,
954
+ width=50,
955
+ height=50,
956
+ name=Annotations.RubberStampAnnotation.final,
957
+ ),
958
+ Annotations.RubberStampAnnotation(
959
+ 1,
960
+ 300,
961
+ 500,
962
+ width=50,
963
+ height=50,
964
+ name=Annotations.RubberStampAnnotation.sold,
965
+ ),
966
+ Annotations.RubberStampAnnotation(
967
+ 1,
968
+ 400,
969
+ 500,
970
+ width=50,
971
+ height=50,
972
+ name=Annotations.RubberStampAnnotation.departmental,
973
+ ),
974
+ Annotations.RubberStampAnnotation(
975
+ 1,
976
+ 0,
977
+ 600,
978
+ width=50,
979
+ height=50,
980
+ name=Annotations.RubberStampAnnotation.for_comment,
981
+ ),
982
+ Annotations.RubberStampAnnotation(
983
+ 1,
984
+ 100,
985
+ 600,
986
+ width=50,
987
+ height=50,
988
+ name=Annotations.RubberStampAnnotation.top_secret,
989
+ ),
990
+ Annotations.RubberStampAnnotation(
991
+ 1,
992
+ 200,
993
+ 600,
994
+ width=50,
995
+ height=50,
996
+ name=Annotations.RubberStampAnnotation.draft,
997
+ ),
998
+ Annotations.RubberStampAnnotation(
999
+ 1,
1000
+ 300,
1001
+ 600,
1002
+ width=50,
1003
+ height=50,
1004
+ name=Annotations.RubberStampAnnotation.for_public_release,
1005
+ ),
1006
+ ]
1007
+ )
1008
+
1009
+ request.config.results["expected_path"] = expected_path
1010
+ request.config.results["stream"] = obj.read()
1011
+
1012
+ expected = f.read()
1013
+
1014
+ assert len(obj.read()) == len(expected)
1015
+ assert obj.read() == expected
File without changes
File without changes
File without changes
File without changes
@@ -35,12 +35,12 @@ class Checkbox(Widget):
35
35
  Attributes:
36
36
  size (int): The size of the checkbox. Defaults to None.
37
37
  """
38
+ super().__init__(name, value)
38
39
  self.SET_ATTR_TRIGGER_HOOK_MAP.update(
39
40
  {
40
41
  "size": "update_check_radio_size",
41
42
  }
42
43
  )
43
- super().__init__(name, value)
44
44
 
45
45
  self.size: Optional[float] = None
46
46
 
@@ -44,15 +44,15 @@ class Dropdown(Widget):
44
44
  font (str): The font of the dropdown field.
45
45
  choices (List[str]): The list of choices for the dropdown.
46
46
  """
47
+ super().__init__(name, value)
47
48
  self.SET_ATTR_TRIGGER_HOOK_MAP.update(
48
49
  {
49
50
  "font": "update_text_field_font",
51
+ "choices": "update_dropdown_choices",
50
52
  "font_size": "update_text_field_font_size",
51
53
  "font_color": "update_text_field_font_color",
52
- "choices": "update_dropdown_choices",
53
54
  }
54
55
  )
55
- super().__init__(name, value)
56
56
 
57
57
  self.font: Optional[str] = None
58
58
  self.font_size: Optional[float] = None
@@ -45,6 +45,7 @@ class Text(Widget):
45
45
  multiline (bool): Whether the text field is multiline. Defaults to None.
46
46
  max_length (int): The maximum length of the text field. Defaults to None.
47
47
  """
48
+ super().__init__(name, value)
48
49
  self.SET_ATTR_TRIGGER_HOOK_MAP.update(
49
50
  {
50
51
  "font": "update_text_field_font",
@@ -56,7 +57,6 @@ class Text(Widget):
56
57
  "max_length": "update_text_field_max_length",
57
58
  }
58
59
  )
59
- super().__init__(name, value)
60
60
 
61
61
  self.font: Optional[str] = None
62
62
  self.font_size: Optional[float] = None
File without changes
File without changes
File without changes
File without changes