PyPDFForm 5.1.1__tar.gz → 5.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {pypdfform-5.1.1 → pypdfform-5.2.1}/PKG-INFO +2 -4
  2. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/common.py +7 -0
  4. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/create.py +9 -5
  5. pypdfform-5.2.1/PyPDFForm/cli/remove.py +33 -0
  6. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/root.py +14 -8
  7. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/update.py +10 -3
  8. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/__init__.py +6 -2
  9. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/base.py +7 -2
  10. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/link.py +7 -2
  11. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/egress.py +1 -2
  12. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/filler.py +10 -4
  13. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/font.py +45 -11
  14. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/hooks.py +39 -7
  15. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/image.py +6 -9
  16. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/patterns.py +39 -6
  17. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/template.py +86 -23
  18. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/watermark.py +16 -7
  19. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/base.py +25 -7
  20. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/signature.py +6 -4
  21. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/wrapper.py +92 -20
  22. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/PKG-INFO +2 -4
  23. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/SOURCES.txt +1 -0
  24. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/requires.txt +1 -3
  25. {pypdfform-5.1.1 → pypdfform-5.2.1}/pyproject.toml +1 -3
  26. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_font_widths.py +10 -3
  27. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_functional.py +13 -0
  28. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_js.py +95 -95
  29. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_use_full_widget_name.py +8 -0
  30. {pypdfform-5.1.1 → pypdfform-5.2.1}/LICENSE +0 -0
  31. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/__init__.py +0 -0
  32. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/entry.py +0 -0
  33. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/inspect.py +0 -0
  34. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/schemas/__init__.py +0 -0
  35. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/schemas/create.py +0 -0
  36. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/cli/schemas/update.py +0 -0
  37. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/__init__.py +0 -0
  38. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/adapter.py +0 -0
  39. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/stamp.py +0 -0
  40. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/text.py +0 -0
  41. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  42. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/assets/__init__.py +0 -0
  43. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/assets/bedrock.py +0 -0
  44. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/assets/blank.py +0 -0
  45. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/constants.py +0 -0
  46. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/coordinate.py +0 -0
  47. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/deprecation.py +0 -0
  48. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/__init__.py +0 -0
  49. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/base.py +0 -0
  50. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  51. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  52. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/image.py +0 -0
  53. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/radio.py +0 -0
  54. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/signature.py +0 -0
  55. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/middleware/text.py +0 -0
  56. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/__init__.py +0 -0
  57. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/circle.py +0 -0
  58. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/ellipse.py +0 -0
  59. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/image.py +0 -0
  60. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/line.py +0 -0
  61. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/rect.py +0 -0
  62. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/raw/text.py +0 -0
  63. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/types.py +0 -0
  64. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/utils.py +0 -0
  65. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/__init__.py +0 -0
  66. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  67. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  68. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/image.py +0 -0
  69. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/radio.py +0 -0
  70. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm/lib/widgets/text.py +0 -0
  71. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  72. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/entry_points.txt +0 -0
  73. {pypdfform-5.1.1 → pypdfform-5.2.1}/PyPDFForm.egg-info/top_level.txt +0 -0
  74. {pypdfform-5.1.1 → pypdfform-5.2.1}/README.md +0 -0
  75. {pypdfform-5.1.1 → pypdfform-5.2.1}/setup.cfg +0 -0
  76. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_bulk_create_fields.py +0 -0
  77. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_create_widget.py +0 -0
  78. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_draw_elements.py +0 -0
  79. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_dropdown.py +0 -0
  80. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_extract_middleware_attributes.py +0 -0
  81. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_fill_max_length_text_field.py +0 -0
  82. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_generate_appearance_streams.py +0 -0
  83. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_need_appearances.py +0 -0
  84. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_paragraph.py +0 -0
  85. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_signature.py +0 -0
  86. {pypdfform-5.1.1 → pypdfform-5.2.1}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 5.1.1
3
+ Version: 5.2.1
4
4
  Summary: The Python library & CLI for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -24,7 +24,7 @@ Classifier: Topic :: Utilities
24
24
  Requires-Python: >=3.10
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
- Requires-Dist: cryptography<49.0.0,>=48.0.0
27
+ Requires-Dist: cryptography<50.0.0,>=48.0.0
28
28
  Requires-Dist: fonttools<5.0.0,>=4.63.0
29
29
  Requires-Dist: pikepdf<11.0.0,>=10.7.2
30
30
  Requires-Dist: pillow<13.0.0,>=12.2.0
@@ -34,9 +34,7 @@ Provides-Extra: cli
34
34
  Requires-Dist: typer<1.0.0,>=0.26.1; extra == "cli"
35
35
  Requires-Dist: jsonschema<5.0.0,>=4.26.0; extra == "cli"
36
36
  Provides-Extra: dev
37
- Requires-Dist: black<27.0.0,>=26.5.1; extra == "dev"
38
37
  Requires-Dist: coverage<8.0.0,>=7.14.1; extra == "dev"
39
- Requires-Dist: isort<9.0.0,>=8.0.1; extra == "dev"
40
38
  Requires-Dist: mike<3.0.0,>=2.2.0; extra == "dev"
41
39
  Requires-Dist: mkdocs<2.0.0,>=1.6.1; extra == "dev"
42
40
  Requires-Dist: mkdocs-material<10.0.0,>=9.7.6; extra == "dev"
@@ -18,7 +18,7 @@ from Python code or from the command line.
18
18
 
19
19
  import logging
20
20
 
21
- __version__ = "5.1.1"
21
+ __version__ = "5.2.1"
22
22
 
23
23
  from .lib.annotations import Annotations
24
24
  from .lib.assets.blank import BlankPage
@@ -53,6 +53,13 @@ OPTIONAL_OUTPUT_PDF = Annotated[
53
53
  ),
54
54
  ]
55
55
  FIELD_NAME = Annotated[str, typer.Option("--field", help="Form field name.")]
56
+ FIELD_NAMES = Annotated[
57
+ list[str],
58
+ typer.Option(
59
+ "--field",
60
+ help="Form field name. Repeat this option to select multiple fields.",
61
+ ),
62
+ ]
56
63
 
57
64
 
58
65
  def json_file_option(help_text: str):
@@ -14,11 +14,15 @@ from typing import Annotated
14
14
 
15
15
  import typer
16
16
 
17
- from .. import (Annotations, BlankPage, Fields, PdfArray, PdfWrapper,
18
- RawElements)
19
- from .common import (INPUT_PDF, OPTIONAL_OUTPUT_PDF, REQUIRED_OUTPUT_PDF,
20
- cli_bad_parameter, create_elements_from_file,
21
- json_file_option)
17
+ from .. import Annotations, BlankPage, Fields, PdfArray, PdfWrapper, RawElements
18
+ from .common import (
19
+ INPUT_PDF,
20
+ OPTIONAL_OUTPUT_PDF,
21
+ REQUIRED_OUTPUT_PDF,
22
+ cli_bad_parameter,
23
+ create_elements_from_file,
24
+ json_file_option,
25
+ )
22
26
  from .schemas.create import ANNOTATION_SCHEMA, FIELD_SCHEMA, RAW_SCHEMA
23
27
 
24
28
  create_cli = typer.Typer(
@@ -0,0 +1,33 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ This module defines CLI commands for removing PDF form content.
4
+
5
+ It exposes the `remove` command group for deleting existing form fields.
6
+ Commands in this module load the target PDF, validate requested form field
7
+ names, apply the matching `PdfWrapper` operation, and write the modified PDF to
8
+ either the requested output path or the original file.
9
+ """
10
+
11
+ import typer
12
+
13
+ from .. import PdfWrapper
14
+ from .common import FIELD_NAMES, INPUT_PDF, OPTIONAL_OUTPUT_PDF, get_widget
15
+
16
+ remove_cli = typer.Typer(
17
+ context_settings={"help_option_names": ["--help", "-h"]}, no_args_is_help=True
18
+ )
19
+
20
+
21
+ @remove_cli.command(no_args_is_help=True)
22
+ def field(
23
+ ctx: typer.Context,
24
+ pdf: INPUT_PDF,
25
+ fields: FIELD_NAMES,
26
+ output: OPTIONAL_OUTPUT_PDF = None,
27
+ ) -> None:
28
+ """Remove form fields from a PDF."""
29
+ obj = PdfWrapper(str(pdf), **ctx.obj)
30
+ for field_name in fields:
31
+ get_widget(obj, field_name, "--field")
32
+
33
+ obj.remove_fields(fields).write(output or pdf)
@@ -2,16 +2,17 @@
2
2
  """
3
3
  This module defines the root command-line interface for PyPDFForm.
4
4
 
5
- It creates the Typer application, attaches the `create`, `inspect`, and `update`
6
- command groups, and exposes top-level options shared by those commands. The root
7
- callback collects global flags in the Typer context so each subcommand can
8
- initialize `PdfWrapper` with consistent settings.
5
+ It creates the Typer application, attaches the `create`, `inspect`, `update`,
6
+ and `remove` command groups, and exposes top-level options shared by those
7
+ commands. The root callback collects global flags in the Typer context so each
8
+ subcommand can initialize `PdfWrapper` with consistent settings.
9
9
 
10
10
  Commands:
11
11
  - `fill`: Fill an existing PDF form from JSON data.
12
12
  - `create`: Create PDFs, fields, annotations, raw elements, and grid views.
13
13
  - `inspect`: Print form metadata and field data as JSON.
14
14
  - `update`: Modify PDF metadata, field names, properties, geometry, and scripts.
15
+ - `remove`: Remove PDF form fields.
15
16
  """
16
17
 
17
18
  from pathlib import Path
@@ -20,10 +21,10 @@ from typing import Annotated
20
21
  import typer
21
22
 
22
23
  from .. import PdfWrapper, Widgets, __version__
23
- from .common import (INPUT_PDF, OPTIONAL_OUTPUT_PDF, json_file_option,
24
- load_json_file)
24
+ from .common import INPUT_PDF, OPTIONAL_OUTPUT_PDF, json_file_option, load_json_file
25
25
  from .create import create_cli
26
26
  from .inspect import inspect_cli
27
+ from .remove import remove_cli
27
28
  from .update import update_cli
28
29
 
29
30
  cli_app = typer.Typer(
@@ -44,6 +45,11 @@ cli_app.add_typer(
44
45
  name="update",
45
46
  help="Update PDF metadata, fields, and scripts.",
46
47
  )
48
+ cli_app.add_typer(
49
+ remove_cli,
50
+ name="remove",
51
+ help="Remove PDF form content.",
52
+ )
47
53
 
48
54
 
49
55
  def version_callback(value: bool) -> None:
@@ -68,7 +74,7 @@ def version_callback(value: bool) -> None:
68
74
 
69
75
  @cli_app.callback(
70
76
  invoke_without_command=True,
71
- help="Create, fill, inspect, and update PDF forms.",
77
+ help="Work with PDF forms from the command line.",
72
78
  )
73
79
  def main(
74
80
  ctx: typer.Context,
@@ -111,7 +117,7 @@ def main(
111
117
  ),
112
118
  ] = False,
113
119
  ) -> None:
114
- """Create, fill, inspect, and update PDF forms."""
120
+ """Work with PDF forms from the command line."""
115
121
  ctx.obj = {
116
122
  "need_appearances": need_appearances,
117
123
  "generate_appearance_streams": generate_appearance_streams,
@@ -17,9 +17,16 @@ import typer
17
17
 
18
18
  from .. import PdfWrapper
19
19
  from ..lib.constants import PdfVersion
20
- from .common import (FIELD_NAME, INPUT_PDF, OPTIONAL_OUTPUT_PDF,
21
- cli_bad_parameter, get_widget, handle_font_registration,
22
- json_file_option, load_json_file)
20
+ from .common import (
21
+ FIELD_NAME,
22
+ INPUT_PDF,
23
+ OPTIONAL_OUTPUT_PDF,
24
+ cli_bad_parameter,
25
+ get_widget,
26
+ handle_font_registration,
27
+ json_file_option,
28
+ load_json_file,
29
+ )
23
30
  from .schemas.update import FIELD_SCHEMA, RENAME_SCHEMA
24
31
 
25
32
  update_cli = typer.Typer(
@@ -14,8 +14,12 @@ from dataclasses import dataclass
14
14
  from .link import LinkAnnotation
15
15
  from .stamp import RubberStampAnnotation
16
16
  from .text import TextAnnotation
17
- from .text_markup import (HighlightAnnotation, SquigglyAnnotation,
18
- StrikeOutAnnotation, UnderlineAnnotation)
17
+ from .text_markup import (
18
+ HighlightAnnotation,
19
+ SquigglyAnnotation,
20
+ StrikeOutAnnotation,
21
+ UnderlineAnnotation,
22
+ )
19
23
 
20
24
  AnnotationTypes = (
21
25
  TextAnnotation
@@ -11,8 +11,13 @@ Classes:
11
11
 
12
12
  from dataclasses import dataclass
13
13
 
14
- from pypdf.generic import (ArrayObject, DictionaryObject, FloatObject,
15
- NameObject, TextStringObject)
14
+ from pypdf.generic import (
15
+ ArrayObject,
16
+ DictionaryObject,
17
+ FloatObject,
18
+ NameObject,
19
+ TextStringObject,
20
+ )
16
21
 
17
22
  from ..constants import Annot, Contents, Rect, Subtype, Type
18
23
 
@@ -12,8 +12,13 @@ Classes:
12
12
  from dataclasses import dataclass
13
13
  from typing import Optional
14
14
 
15
- from pypdf.generic import (ArrayObject, DictionaryObject, NameObject,
16
- NumberObject, TextStringObject)
15
+ from pypdf.generic import (
16
+ ArrayObject,
17
+ DictionaryObject,
18
+ NameObject,
19
+ NumberObject,
20
+ TextStringObject,
21
+ )
17
22
 
18
23
  from ..constants import A, S
19
24
  from .base import Annotation
@@ -17,8 +17,7 @@ from pikepdf import Pdf
17
17
  from pypdf import PdfReader, PdfWriter
18
18
  from pypdf.generic import DictionaryObject, NameObject, TextStringObject
19
19
 
20
- from .constants import (JS, XFA, AcroForm, JavaScript, OpenAction, Root, S,
21
- Title)
20
+ from .constants import JS, XFA, AcroForm, JavaScript, OpenAction, Root, S, Title
22
21
 
23
22
 
24
23
  @lru_cache
@@ -24,9 +24,13 @@ from .middleware.image import Image
24
24
  from .middleware.radio import Radio
25
25
  from .middleware.signature import Signature
26
26
  from .middleware.text import Text
27
- from .patterns import (get_widget_key, update_checkbox_value,
28
- update_dropdown_value, update_radio_value,
29
- update_text_value)
27
+ from .patterns import (
28
+ get_widget_key,
29
+ update_checkbox_value,
30
+ update_dropdown_value,
31
+ update_radio_value,
32
+ update_text_value,
33
+ )
30
34
  from .watermark import create_watermarks_and_draw, merge_watermarks_with_pdf
31
35
 
32
36
 
@@ -53,7 +57,9 @@ def signature_image_handler(
53
57
  any_image_to_draw = False
54
58
  if stream is not None:
55
59
  any_image_to_draw = True
56
- image_width, image_height = get_image_dimensions(stream)
60
+ image_width, image_height = (
61
+ get_image_dimensions(stream) if middleware.preserve_aspect_ratio else (0, 0)
62
+ )
57
63
  x, y, width, height = get_draw_image_resolutions(
58
64
  widget, middleware.preserve_aspect_ratio, image_width, image_height
59
65
  )
@@ -16,20 +16,52 @@ from zlib import compress
16
16
 
17
17
  from fontTools.ttLib import TTFont as FT_TTFont
18
18
  from pypdf import PdfReader, PdfWriter
19
- from pypdf.generic import (ArrayObject, DictionaryObject, FloatObject,
20
- NameObject, NumberObject, StreamObject)
19
+ from pypdf.generic import (
20
+ ArrayObject,
21
+ DictionaryObject,
22
+ FloatObject,
23
+ NameObject,
24
+ NumberObject,
25
+ StreamObject,
26
+ )
21
27
  from reportlab.pdfbase.pdfmetrics import _fonts
22
28
  from reportlab.pdfbase.ttfonts import TTFError, TTFont
23
29
 
24
30
  from .assets.blank import BlankPage
25
- from .constants import (DEFAULT_ASSUMED_GLYPH_WIDTH, DR, EM_TO_PDF_FACTOR,
26
- ENCODING_TABLE_SIZE, FIRST_CHAR_CODE, FONT_NAME_PREFIX,
27
- LAST_CHAR_CODE, AcroForm, BaseFont, Encoding, Fields,
28
- Filter, FirstChar, FlateDecode, Font, FontCmap,
29
- FontDescriptor, FontFile2, FontHead, FontHmtx,
30
- FontName, FontNotdef, LastChar, Length, Length1,
31
- MissingWidth, Resources, Subtype, TrueType, Type,
32
- Widths, WinAnsiEncoding)
31
+ from .constants import (
32
+ DEFAULT_ASSUMED_GLYPH_WIDTH,
33
+ DR,
34
+ EM_TO_PDF_FACTOR,
35
+ ENCODING_TABLE_SIZE,
36
+ FIRST_CHAR_CODE,
37
+ FONT_NAME_PREFIX,
38
+ LAST_CHAR_CODE,
39
+ AcroForm,
40
+ BaseFont,
41
+ Encoding,
42
+ Fields,
43
+ Filter,
44
+ FirstChar,
45
+ FlateDecode,
46
+ Font,
47
+ FontCmap,
48
+ FontDescriptor,
49
+ FontFile2,
50
+ FontHead,
51
+ FontHmtx,
52
+ FontName,
53
+ FontNotdef,
54
+ LastChar,
55
+ Length,
56
+ Length1,
57
+ MissingWidth,
58
+ Resources,
59
+ Subtype,
60
+ TrueType,
61
+ Type,
62
+ Widths,
63
+ WinAnsiEncoding,
64
+ )
33
65
  from .raw.text import RawText
34
66
  from .watermark import create_watermarks_and_draw
35
67
 
@@ -287,7 +319,9 @@ def register_font_acroform(
287
319
  font_dict_ref = writer._add_object(font_dict) # type: ignore # noqa: SLF001 # # pylint: disable=W0212
288
320
 
289
321
  if AcroForm not in writer._root_object: # type: ignore # noqa: SLF001 # # pylint: disable=W0212
290
- writer._root_object[NameObject(AcroForm)] = DictionaryObject({NameObject(Fields): ArrayObject([])}) # type: ignore # noqa: SLF001 # # pylint: disable=W0212
322
+ writer._root_object[NameObject(AcroForm)] = DictionaryObject( # type: ignore # noqa: SLF001 # pylint: disable=W0212
323
+ {NameObject(Fields): ArrayObject([])}
324
+ )
291
325
  acroform = writer._root_object[AcroForm] # type: ignore # noqa: SLF001 # # pylint: disable=W0212
292
326
 
293
327
  if DR not in acroform:
@@ -14,15 +14,47 @@ from io import BytesIO
14
14
  from typing import TextIO, cast
15
15
 
16
16
  from pypdf import PdfReader, PdfWriter
17
- from pypdf.generic import (ArrayObject, DictionaryObject, FloatObject,
18
- NameObject, NumberObject, TextStringObject)
17
+ from pypdf.generic import (
18
+ ArrayObject,
19
+ DictionaryObject,
20
+ FloatObject,
21
+ NameObject,
22
+ NumberObject,
23
+ TextStringObject,
24
+ )
19
25
 
20
26
  from .adapter import fp_or_f_obj_or_f_content_to_content
21
- from .constants import (AA, COMB, DA, FONT_COLOR_IDENTIFIER,
22
- FONT_SIZE_IDENTIFIER, HIDDEN, JS, MULTILINE, READ_ONLY,
23
- REQUIRED, TU, Action, Annots, Bl, D, E, F, Ff, Fo,
24
- JavaScript, MaxLen, Opt, Parent, Q, Rect, S, Type, U,
25
- X)
27
+ from .constants import (
28
+ AA,
29
+ COMB,
30
+ DA,
31
+ FONT_COLOR_IDENTIFIER,
32
+ FONT_SIZE_IDENTIFIER,
33
+ HIDDEN,
34
+ JS,
35
+ MULTILINE,
36
+ READ_ONLY,
37
+ REQUIRED,
38
+ TU,
39
+ Action,
40
+ Annots,
41
+ Bl,
42
+ D,
43
+ E,
44
+ F,
45
+ Ff,
46
+ Fo,
47
+ JavaScript,
48
+ MaxLen,
49
+ Opt,
50
+ Parent,
51
+ Q,
52
+ Rect,
53
+ S,
54
+ Type,
55
+ U,
56
+ X,
57
+ )
26
58
  from .patterns import get_widget_key
27
59
 
28
60
 
@@ -7,6 +7,7 @@ calculating the resolutions for drawing an image on a PDF page, taking into
7
7
  account whether to preserve the aspect ratio.
8
8
  """
9
9
 
10
+ from functools import lru_cache
10
11
  from io import BytesIO
11
12
  from typing import Tuple
12
13
 
@@ -50,12 +51,13 @@ def rotate_image(image_stream: bytes, rotation: float | int) -> bytes:
50
51
  return result
51
52
 
52
53
 
54
+ @lru_cache
53
55
  def get_image_dimensions(image_stream: bytes) -> Tuple[float, float]:
54
56
  """
55
57
  Retrieves the width and height of an image from its byte stream.
56
58
 
57
- This function uses the PIL library to open the image from the provided byte stream
58
- and returns its dimensions (width and height) as a tuple of floats.
59
+ This cached function uses the PIL library to open the image from the provided
60
+ byte stream and returns its dimensions (width and height) as a tuple of floats.
59
61
 
60
62
  Args:
61
63
  image_stream (bytes): The image data as bytes.
@@ -63,13 +65,8 @@ def get_image_dimensions(image_stream: bytes) -> Tuple[float, float]:
63
65
  Returns:
64
66
  Tuple[float, float]: The width and height of the image in pixels.
65
67
  """
66
- buff = BytesIO()
67
- buff.write(image_stream)
68
- buff.seek(0)
69
-
70
- image = Image.open(buff)
71
-
72
- return image.size
68
+ with Image.open(BytesIO(image_stream)) as image:
69
+ return image.size
73
70
 
74
71
 
75
72
  def get_draw_image_resolutions(
@@ -10,12 +10,45 @@ for updating these widgets.
10
10
 
11
11
  from typing import Tuple
12
12
 
13
- from pypdf.generic import (ArrayObject, DictionaryObject, NameObject,
14
- NumberObject, TextStringObject)
15
-
16
- from .constants import (AP, AS, DV, FT, HIDDEN, IMAGE_FIELD_IDENTIFIER, JS,
17
- SLASH, TU, A, Btn, Ch, F, Ff, I, MaxLen, N, Off, Opt,
18
- Parent, Q, Rect, Sig, Subtype, T, Tx, V, Widget, Yes)
13
+ from pypdf.generic import (
14
+ ArrayObject,
15
+ DictionaryObject,
16
+ NameObject,
17
+ NumberObject,
18
+ TextStringObject,
19
+ )
20
+
21
+ from .constants import (
22
+ AP,
23
+ AS,
24
+ DV,
25
+ FT,
26
+ HIDDEN,
27
+ IMAGE_FIELD_IDENTIFIER,
28
+ JS,
29
+ SLASH,
30
+ TU,
31
+ A,
32
+ Btn,
33
+ Ch,
34
+ F,
35
+ Ff,
36
+ I,
37
+ MaxLen,
38
+ N,
39
+ Off,
40
+ Opt,
41
+ Parent,
42
+ Q,
43
+ Rect,
44
+ Sig,
45
+ Subtype,
46
+ T,
47
+ Tx,
48
+ V,
49
+ Widget,
50
+ Yes,
51
+ )
19
52
  from .middleware.checkbox import Checkbox
20
53
  from .middleware.dropdown import Dropdown
21
54
  from .middleware.image import Image
@@ -22,12 +22,22 @@ from .middleware.checkbox import Checkbox
22
22
  from .middleware.dropdown import Dropdown
23
23
  from .middleware.radio import Radio
24
24
  from .middleware.text import Text
25
- from .patterns import (WIDGET_DESCRIPTION_PATTERNS, WIDGET_TYPE_PATTERNS,
26
- check_field_flag, get_checkbox_value,
27
- get_dropdown_choices, get_dropdown_value,
28
- get_field_hidden, get_field_rect, get_radio_value,
29
- get_text_field_alignment, get_text_field_max_length,
30
- get_text_value, get_widget_key, update_annotation_name)
25
+ from .patterns import (
26
+ WIDGET_DESCRIPTION_PATTERNS,
27
+ WIDGET_TYPE_PATTERNS,
28
+ check_field_flag,
29
+ get_checkbox_value,
30
+ get_dropdown_choices,
31
+ get_dropdown_value,
32
+ get_field_hidden,
33
+ get_field_rect,
34
+ get_radio_value,
35
+ get_text_field_alignment,
36
+ get_text_field_max_length,
37
+ get_text_value,
38
+ get_widget_key,
39
+ update_annotation_name,
40
+ )
31
41
  from .utils import extract_widget_property, find_pattern_match
32
42
 
33
43
 
@@ -366,6 +376,51 @@ def create_annotations(
366
376
  return f.read()
367
377
 
368
378
 
379
+ def remove_widgets_by_keys(
380
+ pdf: bytes, keys: List[str], use_full_widget_name: bool = False
381
+ ) -> bytes:
382
+ """
383
+ Removes specific widgets from a PDF by their keys.
384
+
385
+ This function removes any widget annotation whose key matches one of the
386
+ provided keys. If no keys are provided, the original PDF stream is returned
387
+ unchanged.
388
+
389
+ Args:
390
+ pdf (bytes): The PDF stream to remove widgets from.
391
+ keys (List[str]): A list of widget keys to remove.
392
+ use_full_widget_name (bool): Whether to match widgets by their full
393
+ names, including parent names.
394
+
395
+ Returns:
396
+ bytes: The updated PDF stream with the matching widgets removed.
397
+ """
398
+ if not keys:
399
+ return pdf
400
+
401
+ writer = PdfWriter(BytesIO(pdf))
402
+
403
+ for page in writer.pages:
404
+ needs_update = False
405
+ page_annots = ArrayObject([])
406
+
407
+ for annot in page.get(Annots, []):
408
+ annot = cast(DictionaryObject, annot.get_object())
409
+ key = get_widget_key(annot.get_object(), use_full_widget_name)
410
+ if key not in keys:
411
+ page_annots.append(annot)
412
+ else:
413
+ needs_update = True
414
+
415
+ if needs_update:
416
+ page[NameObject(Annots)] = page_annots
417
+
418
+ with BytesIO() as f:
419
+ writer.write(f)
420
+ f.seek(0)
421
+ return f.read()
422
+
423
+
369
424
  def update_widget_keys(
370
425
  template: bytes,
371
426
  widgets: Dict[str, WIDGET_TYPES],
@@ -394,8 +449,7 @@ def update_widget_keys(
394
449
  out = PdfWriter()
395
450
  out.append(pdf)
396
451
 
397
- for i, old_key in enumerate(old_keys):
398
- _update_single_widget_key(out, widgets, old_key, new_keys[i], indices[i])
452
+ _apply_widget_key_updates(out, widgets, old_keys, new_keys, indices)
399
453
 
400
454
  with BytesIO() as f:
401
455
  out.write(f)
@@ -403,35 +457,44 @@ def update_widget_keys(
403
457
  return f.read()
404
458
 
405
459
 
406
- def _update_single_widget_key(
460
+ def _apply_widget_key_updates(
407
461
  writer: PdfWriter,
408
462
  widgets: Dict[str, WIDGET_TYPES],
409
- old_key: str,
410
- new_key: str,
411
- index: int,
463
+ old_keys: List[str],
464
+ new_keys: List[str],
465
+ indices: List[int],
412
466
  ) -> None:
413
467
  """
414
- Updates a single widget key in a PDF template.
468
+ Applies queued widget key updates to matching annotations.
469
+
470
+ The update queue is converted into a lookup keyed by old widget name, then
471
+ each annotation is checked against that lookup as pages are traversed.
472
+ Non-radio widgets honor the requested occurrence index, while radio widgets
473
+ update every annotation in the radio group.
415
474
 
416
475
  Args:
417
476
  writer (PdfWriter): The PDF writer object.
418
477
  widgets (Dict[str, WIDGET_TYPES]): A dictionary of widgets in the template.
419
- old_key (str): The old widget key to be replaced.
420
- new_key (str): The new widget key to replace the old key.
421
- index (int): The index of the widget to update if multiple widgets have the same name.
478
+ old_keys (List[str]): The old widget keys to replace.
479
+ new_keys (List[str]): The new widget keys to apply.
480
+ indices (List[int]): Widget occurrence indices for duplicate field names.
422
481
  """
423
- tracker = -1
482
+ updates = {old_key: (new_keys[i], indices[i]) for i, old_key in enumerate(old_keys)}
483
+ trackers = {}
484
+
424
485
  for page in writer.pages:
425
486
  for annot in page.get(Annots, []):
426
487
  annot = cast(DictionaryObject, annot.get_object())
427
488
  key = get_widget_key(annot.get_object(), False)
428
489
 
429
- widget = widgets.get(key)
430
- if widget is None or old_key != key:
490
+ if key not in updates:
431
491
  continue
432
492
 
433
- tracker += 1
434
- if not isinstance(widget, Radio) and tracker != index:
435
- continue
493
+ widget = widgets.get(key)
494
+ if widget is not None:
495
+ trackers[key] = trackers.get(key, -1) + 1
496
+ new_key, index = updates[key]
497
+ if not isinstance(widget, Radio) and trackers[key] != index:
498
+ continue
436
499
 
437
- update_annotation_name(annot, new_key)
500
+ update_annotation_name(annot, new_key)