PyPDFForm 5.2.4__tar.gz → 5.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {pypdfform-5.2.4 → pypdfform-5.2.5}/PKG-INFO +1 -1
  2. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/__init__.py +1 -1
  3. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/egress.py +98 -12
  4. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/utils.py +32 -14
  5. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/wrapper.py +4 -2
  6. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm.egg-info/PKG-INFO +1 -1
  7. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_functional.py +16 -4
  8. {pypdfform-5.2.4 → pypdfform-5.2.5}/LICENSE +0 -0
  9. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/__init__.py +0 -0
  10. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/common.py +0 -0
  11. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/create.py +0 -0
  12. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/entry.py +0 -0
  13. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/inspect.py +0 -0
  14. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/remove.py +0 -0
  15. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/root.py +0 -0
  16. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/schemas/__init__.py +0 -0
  17. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/schemas/create.py +0 -0
  18. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/schemas/update.py +0 -0
  19. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/cli/update.py +0 -0
  20. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/__init__.py +0 -0
  21. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/adapter.py +0 -0
  22. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/annotations/__init__.py +0 -0
  23. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/annotations/base.py +0 -0
  24. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/annotations/link.py +0 -0
  25. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/annotations/stamp.py +0 -0
  26. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/annotations/text.py +0 -0
  27. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/annotations/text_markup.py +0 -0
  28. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/assets/__init__.py +0 -0
  29. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/assets/bedrock.py +0 -0
  30. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/assets/blank.py +0 -0
  31. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/constants.py +0 -0
  32. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/coordinate.py +0 -0
  33. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/deprecation.py +0 -0
  34. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/filler.py +0 -0
  35. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/font.py +0 -0
  36. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/hooks.py +0 -0
  37. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/image.py +0 -0
  38. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/middleware/__init__.py +0 -0
  39. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/middleware/base.py +0 -0
  40. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/middleware/checkbox.py +0 -0
  41. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/middleware/dropdown.py +0 -0
  42. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/middleware/image.py +0 -0
  43. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/middleware/radio.py +0 -0
  44. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/middleware/signature.py +0 -0
  45. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/middleware/text.py +0 -0
  46. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/patterns.py +0 -0
  47. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/raw/__init__.py +0 -0
  48. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/raw/circle.py +0 -0
  49. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/raw/ellipse.py +0 -0
  50. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/raw/image.py +0 -0
  51. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/raw/line.py +0 -0
  52. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/raw/rect.py +0 -0
  53. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/raw/text.py +0 -0
  54. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/template.py +0 -0
  55. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/types.py +0 -0
  56. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/watermark.py +0 -0
  57. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/widgets/__init__.py +0 -0
  58. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/widgets/base.py +0 -0
  59. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/widgets/checkbox.py +0 -0
  60. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/widgets/dropdown.py +0 -0
  61. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/widgets/image.py +0 -0
  62. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/widgets/radio.py +0 -0
  63. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/widgets/signature.py +0 -0
  64. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm/lib/widgets/text.py +0 -0
  65. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  66. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  67. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm.egg-info/entry_points.txt +0 -0
  68. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm.egg-info/requires.txt +0 -0
  69. {pypdfform-5.2.4 → pypdfform-5.2.5}/PyPDFForm.egg-info/top_level.txt +0 -0
  70. {pypdfform-5.2.4 → pypdfform-5.2.5}/README.md +0 -0
  71. {pypdfform-5.2.4 → pypdfform-5.2.5}/pyproject.toml +0 -0
  72. {pypdfform-5.2.4 → pypdfform-5.2.5}/setup.cfg +0 -0
  73. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_bulk_create_fields.py +0 -0
  74. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_create_widget.py +0 -0
  75. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_draw_elements.py +0 -0
  76. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_dropdown.py +0 -0
  77. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_extract_middleware_attributes.py +0 -0
  78. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_fill_max_length_text_field.py +0 -0
  79. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_font_widths.py +0 -0
  80. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_generate_appearance_streams.py +0 -0
  81. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_js.py +0 -0
  82. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_need_appearances.py +0 -0
  83. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_paragraph.py +0 -0
  84. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_signature.py +0 -0
  85. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_use_full_widget_name.py +0 -0
  86. {pypdfform-5.2.4 → pypdfform-5.2.5}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 5.2.4
3
+ Version: 5.2.5
4
4
  Summary: The Python library & CLI for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -18,7 +18,7 @@ from Python code or from the command line.
18
18
 
19
19
  import logging
20
20
 
21
- __version__ = "5.2.4"
21
+ __version__ = "5.2.5"
22
22
 
23
23
  from .lib.annotations import Annotations
24
24
  from .lib.assets.blank import BlankPage
@@ -28,6 +28,7 @@ from .constants import (
28
28
  Fields,
29
29
  JavaScript,
30
30
  OpenAction,
31
+ Parent,
31
32
  Root,
32
33
  S,
33
34
  Title,
@@ -140,10 +141,10 @@ def rebuild_acroform_fields(
140
141
 
141
142
  The existing `/Fields` array is replaced, creating an AcroForm dictionary
142
143
  when necessary. Each page annotation is resolved to a widget key, and only
143
- annotations whose keys are present in `widget_keys` are added to the new
144
- array. Page annotation arrays are left unchanged. When no matching page
145
- annotations are found, the original PDF stream is returned unchanged to
146
- avoid an unnecessary rewrite.
144
+ annotations whose keys are present in `widget_keys` contribute their
145
+ top-level field object to the new array. Page annotation arrays are left
146
+ unchanged. When no matching page annotations are found, the original PDF
147
+ stream is returned unchanged to avoid an unnecessary rewrite.
147
148
 
148
149
  Args:
149
150
  pdf (bytes): The PDF stream whose AcroForm fields should be rebuilt.
@@ -155,25 +156,110 @@ def rebuild_acroform_fields(
155
156
  bytes: The PDF stream with a rebuilt AcroForm `/Fields` array, or the
156
157
  original stream when there are no matching widgets to rebuild.
157
158
  """
159
+ if not widget_keys:
160
+ return pdf
161
+
158
162
  writer = PdfWriter(BytesIO(pdf))
159
163
  root = writer._root_object # type: ignore # noqa: SLF001 # # pylint: disable=W0212
160
164
 
161
- if AcroForm not in root:
162
- root[NameObject(AcroForm)] = DictionaryObject({})
163
- root[AcroForm][NameObject(Fields)] = ArrayObject([])
164
-
165
- needs_update = False
165
+ fields = ArrayObject([])
166
+ seen_fields = set()
166
167
  for page in writer.pages:
167
168
  for annot in page.get(Annots, []):
168
169
  key = get_widget_key(annot.get_object(), use_full_widget_name)
169
170
  if key in widget_keys:
170
- root[AcroForm][Fields].append(annot)
171
- needs_update = True
171
+ field_ref = _get_root_field_reference(writer, annot)
172
+ field_key = _field_reference_key(field_ref)
173
+ if field_key not in seen_fields:
174
+ fields.append(field_ref)
175
+ seen_fields.add(field_key)
172
176
 
173
- if not needs_update:
177
+ if not seen_fields:
174
178
  return pdf
175
179
 
180
+ if AcroForm not in root:
181
+ root[NameObject(AcroForm)] = DictionaryObject({})
182
+ root[AcroForm][NameObject(Fields)] = fields
183
+
176
184
  with BytesIO() as f:
177
185
  writer.write(f)
178
186
  f.seek(0)
179
187
  return f.read()
188
+
189
+
190
+ def _get_root_field_reference(writer: PdfWriter, annot):
191
+ """
192
+ Returns the top-level AcroForm field reference for an annotation.
193
+
194
+ Widget annotations can be leaf nodes under a parent field dictionary. The
195
+ catalog `/AcroForm/Fields` array must point at root fields rather than
196
+ child widgets, otherwise readers such as pypdf will not expose hierarchical
197
+ fields like radio groups via `get_fields`.
198
+
199
+ Args:
200
+ writer (PdfWriter): The PDF writer that owns the annotation objects.
201
+ annot: The widget annotation reference or object.
202
+
203
+ Returns:
204
+ IndirectObject: The top-level AcroForm field reference.
205
+ """
206
+ field = annot
207
+ field_object = field.get_object()
208
+ visited = set()
209
+
210
+ while Parent in field_object and id(field_object) not in visited:
211
+ visited.add(id(field_object))
212
+ field = field_object[Parent]
213
+ field_object = field.get_object()
214
+
215
+ return _ensure_indirect_reference(writer, field)
216
+
217
+
218
+ def _ensure_indirect_reference(writer: PdfWriter, field):
219
+ """
220
+ Returns an indirect reference for a field object.
221
+
222
+ pypdf expects entries in `/AcroForm/Fields` to have an indirect reference
223
+ when it builds field objects. Parent fields cloned from page annotations can
224
+ appear as direct dictionaries that still know their original indirect
225
+ reference, so prefer that before adding a new indirect object.
226
+
227
+ Args:
228
+ writer (PdfWriter): The PDF writer that will own newly added objects.
229
+ field: The field reference or direct field object to normalize.
230
+
231
+ Returns:
232
+ IndirectObject: An indirect reference to the field object.
233
+ """
234
+ field_object = field.get_object()
235
+ indirect_reference = getattr(field_object, "indirect_reference", None)
236
+ if indirect_reference is not None:
237
+ return indirect_reference
238
+
239
+ result = field
240
+ if not hasattr(field, "idnum"):
241
+ result = writer._add_object(field_object) # type: ignore[attr-defined] # noqa: SLF001 # pylint: disable=W0212
242
+
243
+ return result
244
+
245
+
246
+ def _field_reference_key(field_ref) -> tuple:
247
+ """
248
+ Returns a stable key for deduplicating root fields in one writer.
249
+
250
+ Args:
251
+ field_ref: The field reference to identify.
252
+
253
+ Returns:
254
+ tuple: A stable key for the field reference.
255
+ """
256
+ field_object = field_ref.get_object()
257
+ indirect_reference = getattr(field_object, "indirect_reference", field_ref)
258
+ idnum = getattr(indirect_reference, "idnum", None)
259
+ generation = getattr(indirect_reference, "generation", None)
260
+
261
+ result = (id(field_object),)
262
+ if idnum is not None:
263
+ result = idnum, generation
264
+
265
+ return result
@@ -354,16 +354,15 @@ def get_version(pdf: bytes) -> str | None:
354
354
  start with a known PDF version identifier.
355
355
  """
356
356
 
357
- result = None
358
- for each in VERSION_IDENTIFIERS:
359
- if pdf.startswith(each):
360
- result = each.replace(VERSION_IDENTIFIER_PREFIX, b"").decode()
361
- break
357
+ version_identifier_length = len(VERSION_IDENTIFIERS[0])
358
+ version_identifier = pdf[:version_identifier_length]
359
+ if version_identifier not in VERSION_IDENTIFIERS:
360
+ return None
362
361
 
363
- return result
362
+ return version_identifier[len(VERSION_IDENTIFIER_PREFIX) :].decode()
364
363
 
365
364
 
366
- def set_version(pdf: bytes, old: str, new: str) -> bytes:
365
+ def set_version(pdf: bytes, old: str | None, new: str | None) -> bytes:
367
366
  """
368
367
  Replaces the first PDF header version marker in a byte stream.
369
368
 
@@ -374,15 +373,34 @@ def set_version(pdf: bytes, old: str, new: str) -> bytes:
374
373
 
375
374
  Args:
376
375
  pdf (bytes): The PDF stream to update.
377
- old (str): The currently present PDF version string.
378
- new (str): The PDF version string to write into the header.
376
+ old (str | None): The currently present PDF version string. When
377
+ None, the header token after `%PDF-` is replaced if one exists.
378
+ new (str | None): The PDF version string to write into the header. When
379
+ None, the PDF stream is returned unchanged.
379
380
 
380
381
  Returns:
381
382
  bytes: The PDF stream with the first matching version marker replaced.
382
383
  """
383
384
 
384
- return pdf.replace(
385
- VERSION_IDENTIFIER_PREFIX + bytes(old, "utf-8"),
386
- VERSION_IDENTIFIER_PREFIX + bytes(new, "utf-8"),
387
- 1,
388
- )
385
+ if new is None or old == new:
386
+ return pdf
387
+
388
+ new_header = VERSION_IDENTIFIER_PREFIX + new.encode()
389
+
390
+ if old is None:
391
+ if not pdf.startswith(VERSION_IDENTIFIER_PREFIX):
392
+ return pdf
393
+
394
+ header_end = len(pdf)
395
+ for separator in (b"\r", b"\n"):
396
+ separator_index = pdf.find(separator, len(VERSION_IDENTIFIER_PREFIX))
397
+ if separator_index != -1:
398
+ header_end = min(header_end, separator_index)
399
+
400
+ return new_header + pdf[header_end:]
401
+
402
+ old_header = VERSION_IDENTIFIER_PREFIX + old.encode()
403
+ if pdf.startswith(old_header):
404
+ return new_header + pdf[len(old_header) :]
405
+
406
+ return pdf.replace(old_header, new_header, 1)
@@ -585,8 +585,10 @@ class PdfWrapper:
585
585
  PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
586
586
  """
587
587
 
588
- if self.version:
589
- self._stream = set_version(self._read(), self.version, version)
588
+ current_stream = self._read()
589
+ updated_stream = set_version(current_stream, self.version, version)
590
+ if updated_stream != current_stream:
591
+ self._stream = updated_stream
590
592
  self._version = version
591
593
 
592
594
  return self
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 5.2.4
3
+ Version: 5.2.5
4
4
  Summary: The Python library & CLI for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -21,6 +21,7 @@ from PyPDFForm.lib.constants import (
21
21
  from PyPDFForm.lib.deprecation import deprecation_notice
22
22
  from PyPDFForm.lib.middleware.base import Widget
23
23
  from PyPDFForm.lib.template import get_widget_key, get_widgets_by_page
24
+ from PyPDFForm.lib.utils import get_version, set_version
24
25
 
25
26
 
26
27
  def test_deprecation_warning():
@@ -113,10 +114,11 @@ def test_write(template_stream, tmp_path):
113
114
 
114
115
  def test_write_io(template_stream):
115
116
  buff = BytesIO()
116
- PdfWrapper(template_stream).write(buff)
117
+ obj = PdfWrapper(template_stream)
118
+ obj.write(buff)
117
119
  buff.seek(0)
118
120
 
119
- assert buff.read()
121
+ assert PdfWrapper(buff.read()).widgets.keys() == obj.widgets.keys()
120
122
 
121
123
 
122
124
  def test_fill_flatten_then_unflatten(template_stream, pdf_samples, data_dict, request):
@@ -507,7 +509,14 @@ def test_version(pdf_samples):
507
509
 
508
510
  obj = PdfWrapper(os.path.join(pdf_samples, "versions", "unknown.pdf"))
509
511
  assert obj.version is None
510
- assert obj.read()
512
+ stream = obj.read()
513
+ assert stream.startswith(b"%PDF-2.1")
514
+ assert get_version(b"not a pdf") is None
515
+ assert set_version(stream, obj.version, None) == stream
516
+ assert set_version(b"not a pdf", None, "2.0") == b"not a pdf"
517
+ assert set_version(b"prefix %PDF-1.7\n", "1.7", "2.0") == b"prefix %PDF-2.0\n"
518
+ assert obj.change_version("2.0").version == "2.0"
519
+ assert obj.read().startswith(b"%PDF-2.0")
511
520
 
512
521
 
513
522
  @pytest.mark.requires_zlib_over_zlib_ng
@@ -855,7 +864,10 @@ def test_remove_fields_update_widgets(template_stream):
855
864
 
856
865
 
857
866
  def test_remove_fields_no_keys_specified(template_stream):
858
- assert PdfWrapper(template_stream).remove_fields([]).read()
867
+ assert (
868
+ PdfWrapper(template_stream).remove_fields([]).widgets.keys()
869
+ == PdfWrapper(template_stream).widgets.keys()
870
+ )
859
871
 
860
872
 
861
873
  def test_merge(template_stream):
File without changes
File without changes
File without changes
File without changes
File without changes