PyPDFForm 3.7.2__tar.gz → 3.7.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.
Files changed (57) hide show
  1. {pypdfform-3.7.2 → pypdfform-3.7.4}/PKG-INFO +2 -2
  2. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/__init__.py +1 -1
  3. pypdfform-3.7.4/PyPDFForm/ap.py +64 -0
  4. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/constants.py +0 -4
  5. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/wrapper.py +36 -63
  6. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/PKG-INFO +2 -2
  7. {pypdfform-3.7.2 → pypdfform-3.7.4}/README.md +1 -1
  8. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_generate_appearance_streams.py +3 -35
  9. pypdfform-3.7.2/PyPDFForm/ap.py +0 -182
  10. {pypdfform-3.7.2 → pypdfform-3.7.4}/LICENSE +0 -0
  11. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/adapter.py +0 -0
  12. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/coordinate.py +0 -0
  13. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/deprecation.py +0 -0
  14. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/filler.py +0 -0
  15. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/font.py +0 -0
  16. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/hooks.py +0 -0
  17. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/image.py +0 -0
  18. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/__init__.py +0 -0
  19. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/base.py +0 -0
  20. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/checkbox.py +0 -0
  21. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/dropdown.py +0 -0
  22. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/image.py +0 -0
  23. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/radio.py +0 -0
  24. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/signature.py +0 -0
  25. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/text.py +0 -0
  26. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/patterns.py +0 -0
  27. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/template.py +0 -0
  28. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/utils.py +0 -0
  29. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/watermark.py +0 -0
  30. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/__init__.py +0 -0
  31. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/base.py +0 -0
  32. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/bedrock.py +0 -0
  33. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/checkbox.py +0 -0
  34. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/dropdown.py +0 -0
  35. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/image.py +0 -0
  36. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/radio.py +0 -0
  37. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/signature.py +0 -0
  38. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/text.py +0 -0
  39. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/SOURCES.txt +0 -0
  40. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/dependency_links.txt +0 -0
  41. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/requires.txt +0 -0
  42. {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/top_level.txt +0 -0
  43. {pypdfform-3.7.2 → pypdfform-3.7.4}/pyproject.toml +0 -0
  44. {pypdfform-3.7.2 → pypdfform-3.7.4}/setup.cfg +0 -0
  45. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_bulk_create_fields.py +0 -0
  46. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_create_widget.py +0 -0
  47. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_dropdown.py +0 -0
  48. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_extract_values.py +0 -0
  49. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_fill_max_length_text_field.py +0 -0
  50. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_fill_method.py +0 -0
  51. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_font_widths.py +0 -0
  52. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_functional.py +0 -0
  53. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_need_appearances.py +0 -0
  54. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_paragraph.py +0 -0
  55. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_signature.py +0 -0
  56. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_use_full_widget_name.py +0 -0
  57. {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_widget_attr_trigger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 3.7.2
3
+ Version: 3.7.4
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -79,7 +79,7 @@ A sample PDF form can be found [here](https://chinapandaman.github.io/PyPDFForm/
79
79
  ```python
80
80
  from PyPDFForm import PdfWrapper
81
81
 
82
- filled = PdfWrapper("sample_template.pdf", generate_appearance_streams=True).fill(
82
+ filled = PdfWrapper("sample_template.pdf", need_appearances=True).fill(
83
83
  {
84
84
  "test": "test_1",
85
85
  "check": True,
@@ -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.7.2"
23
+ __version__ = "3.7.4"
24
24
 
25
25
  from .middleware.text import Text # exposing for setting global font attrs
26
26
  from .widgets import Fields
@@ -0,0 +1,64 @@
1
+ # -*- coding: utf-8 -*-
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.
8
+ """
9
+
10
+ from functools import lru_cache
11
+ from io import BytesIO
12
+
13
+ from pikepdf import Pdf
14
+ from pypdf import PdfReader, PdfWriter
15
+
16
+ from .constants import XFA, AcroForm, Root
17
+ from .utils import stream_to_io
18
+
19
+
20
+ @lru_cache
21
+ def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) -> bytes:
22
+ """
23
+ Handles appearance streams and the /NeedAppearances flag for a PDF form.
24
+
25
+ This function prepares a PDF for form filling by:
26
+ 1. Removing the XFA dictionary if present, as it can interfere with standard
27
+ AcroForm processing.
28
+ 2. Setting the /NeedAppearances flag in the AcroForm dictionary, which instructs
29
+ PDF viewers to generate appearance streams for form fields.
30
+ 3. Optionally generating appearance streams explicitly using pikepdf if
31
+ `generate_appearance_streams` is True.
32
+
33
+ The result is cached using lru_cache for performance.
34
+
35
+ Args:
36
+ pdf (bytes): The PDF file content as a bytes stream.
37
+ generate_appearance_streams (bool): Whether to explicitly generate appearance streams for all form fields.
38
+
39
+ Returns:
40
+ bytes: The modified PDF content as a bytes stream.
41
+ """
42
+ reader = PdfReader(stream_to_io(pdf))
43
+ writer = PdfWriter()
44
+
45
+ if AcroForm in reader.trailer[Root] and XFA in reader.trailer[Root][AcroForm]:
46
+ del reader.trailer[Root][AcroForm][XFA]
47
+
48
+ writer.append(reader)
49
+ writer.set_need_appearances_writer()
50
+
51
+ with BytesIO() as f:
52
+ writer.write(f)
53
+ f.seek(0)
54
+ result = f.read()
55
+
56
+ if generate_appearance_streams:
57
+ with Pdf.open(stream_to_io(result)) as f:
58
+ f.generate_appearance_streams()
59
+ with BytesIO() as r:
60
+ f.save(r)
61
+ r.seek(0)
62
+ result = r.read()
63
+
64
+ return result
@@ -92,10 +92,6 @@ FontCmap = "cmap"
92
92
  FontHmtx = "hmtx"
93
93
  FontNotdef = ".notdef"
94
94
 
95
- # AP stream
96
- BBox = "/BBox"
97
- Td = b"Td"
98
-
99
95
  FIRST_CHAR_CODE = 0
100
96
  LAST_CHAR_CODE = 255
101
97
  ENCODING_TABLE_SIZE = 256
@@ -24,7 +24,7 @@ from functools import cached_property
24
24
  from typing import TYPE_CHECKING, BinaryIO, Dict, List, Sequence, Tuple, Union
25
25
 
26
26
  from .adapter import fp_or_f_obj_or_stream_to_stream
27
- from .ap import appearance_streams_handler, appearance_streams_post_processing
27
+ from .ap import appearance_streams_handler
28
28
  from .constants import (DEFAULT_FONT, DEFAULT_FONT_COLOR, DEFAULT_FONT_SIZE,
29
29
  VERSION_IDENTIFIER_PREFIX, VERSION_IDENTIFIERS)
30
30
  from .coordinate import generate_coordinate_grid
@@ -141,10 +141,10 @@ class PdfWrapper:
141
141
  PdfWrapper: A new `PdfWrapper` object containing the merged PDFs.
142
142
  """
143
143
 
144
- if not self._read():
144
+ if not self.read():
145
145
  return other
146
146
 
147
- if not other._read():
147
+ if not other.read():
148
148
  return self
149
149
 
150
150
  unique_suffix = generate_unique_suffix()
@@ -156,7 +156,7 @@ class PdfWrapper:
156
156
 
157
157
  # user params are based on the first object
158
158
  result = self.__class__(
159
- merge_two_pdfs(self._read(), other._read()),
159
+ merge_two_pdfs(self.read(), other.read()),
160
160
  **{each[0]: getattr(self, each[0], each[1]) for each in self.USER_PARAMS},
161
161
  )
162
162
 
@@ -177,10 +177,10 @@ class PdfWrapper:
177
177
 
178
178
  new_widgets = (
179
179
  build_widgets(
180
- self._read(),
180
+ self.read(),
181
181
  getattr(self, "use_full_widget_name"),
182
182
  )
183
- if self._read()
183
+ if self.read()
184
184
  else {}
185
185
  )
186
186
  # ensure old widgets don't get overwritten
@@ -200,8 +200,8 @@ class PdfWrapper:
200
200
 
201
201
  self.widgets = new_widgets
202
202
 
203
- if self._read():
204
- self._available_fonts.update(**get_all_available_fonts(self._read()))
203
+ if self.read():
204
+ self._available_fonts.update(**get_all_available_fonts(self.read()))
205
205
 
206
206
  def _reregister_font(self) -> PdfWrapper:
207
207
  """
@@ -279,7 +279,7 @@ class PdfWrapper:
279
279
  """
280
280
 
281
281
  for each in VERSION_IDENTIFIERS:
282
- if self._read().startswith(each):
282
+ if self.read().startswith(each):
283
283
  return each.replace(VERSION_IDENTIFIER_PREFIX, b"").decode()
284
284
 
285
285
  return None
@@ -308,10 +308,10 @@ class PdfWrapper:
308
308
 
309
309
  result = [
310
310
  self.__class__(
311
- copy_watermark_widgets(each, self._read(), None, i),
311
+ copy_watermark_widgets(each, self.read(), None, i),
312
312
  **{param: getattr(self, param) for param, _ in self.USER_PARAMS},
313
313
  )
314
- for i, each in enumerate(get_page_streams(remove_all_widgets(self._read())))
314
+ for i, each in enumerate(get_page_streams(remove_all_widgets(self.read())))
315
315
  ]
316
316
 
317
317
  # because copy_watermark_widgets and remove_all_widgets
@@ -324,41 +324,15 @@ class PdfWrapper:
324
324
 
325
325
  def read(self) -> bytes:
326
326
  """
327
- Returns the current PDF content as a byte string.
327
+ Reads the PDF content from the underlying stream.
328
328
 
329
- This method retrieves the PDF content, ensuring all necessary pre-read
330
- operations (like widget hook execution and setting /NeedAppearances) are
331
- complete. If appearance streams were explicitly generated (via
332
- `generate_appearance_streams=True`), it performs post-processing, such as
333
- correcting text alignment, before returning the final PDF data.
329
+ This method returns the current state of the PDF as a byte string.
330
+ It also triggers any pending widget hooks and applies necessary PDF settings
331
+ like setting the `NeedAppearances` flag or generating appearance streams
332
+ if configured.
334
333
 
335
334
  Returns:
336
- bytes: The byte string representation of the PDF document.
337
- """
338
-
339
- result = self._read()
340
-
341
- if getattr(self, "generate_appearance_streams") and result:
342
- result = appearance_streams_post_processing(
343
- result,
344
- self.widgets,
345
- getattr(self, "use_full_widget_name"),
346
- self._available_fonts,
347
- )
348
-
349
- return result
350
-
351
- def _read(self) -> bytes:
352
- """
353
- Internal method to retrieve the raw PDF content stream.
354
-
355
- This method applies necessary pre-read modifications to the PDF stream,
356
- such as triggering widget hooks for dynamic content and ensuring
357
- appearance stream handling (setting the NeedAppearances flag) is
358
- performed if configured.
359
-
360
- Returns:
361
- bytes: The raw byte string of the PDF document stream.
335
+ bytes: The PDF content as bytes.
362
336
  """
363
337
 
364
338
  if any(widget.hooks_to_trigger for widget in self.widgets.values()):
@@ -378,7 +352,6 @@ class PdfWrapper:
378
352
  getattr(self, "use_full_widget_name"),
379
353
  )
380
354
 
381
- # TODO: move to public .read()
382
355
  if getattr(self, "need_appearances") and self._stream:
383
356
  self._stream = appearance_streams_handler(
384
357
  self._stream, getattr(self, "generate_appearance_streams")
@@ -413,7 +386,7 @@ class PdfWrapper:
413
386
  PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
414
387
  """
415
388
 
416
- self._stream = self._read().replace(
389
+ self._stream = self.read().replace(
417
390
  VERSION_IDENTIFIER_PREFIX + bytes(self.version, "utf-8"),
418
391
  VERSION_IDENTIFIER_PREFIX + bytes(version, "utf-8"),
419
392
  1,
@@ -435,10 +408,10 @@ class PdfWrapper:
435
408
  PdfWrapper: The `PdfWrapper` object, allowing for method chaining.
436
409
  """
437
410
 
438
- stream_with_widgets = self._read()
411
+ stream_with_widgets = self.read()
439
412
  self._stream = copy_watermark_widgets(
440
413
  generate_coordinate_grid(
441
- remove_all_widgets(self._read()),
414
+ remove_all_widgets(self.read()),
442
415
  color,
443
416
  margin,
444
417
  ),
@@ -474,7 +447,7 @@ class PdfWrapper:
474
447
  self.widgets[key].value = value
475
448
 
476
449
  filled_stream, image_drawn_stream = fill(
477
- self._read(),
450
+ self.read(),
478
451
  self.widgets,
479
452
  need_appearances=getattr(self, "need_appearances"),
480
453
  use_full_widget_name=getattr(self, "use_full_widget_name"),
@@ -581,9 +554,9 @@ class PdfWrapper:
581
554
  )
582
555
  )
583
556
 
584
- watermarks = getattr(widget_class, "bulk_watermarks")(widgets, self._read())
557
+ watermarks = getattr(widget_class, "bulk_watermarks")(widgets, self.read())
585
558
  self._stream = copy_watermark_widgets(
586
- self._read(),
559
+ self.read(),
587
560
  watermarks,
588
561
  [widget.name for widget in widgets],
589
562
  None,
@@ -688,9 +661,9 @@ class PdfWrapper:
688
661
  return self
689
662
 
690
663
  obj = _class(name=name, page_number=page_number, x=x, y=y, **kwargs)
691
- watermarks = obj.watermarks(self._read())
664
+ watermarks = obj.watermarks(self.read())
692
665
 
693
- self._stream = copy_watermark_widgets(self._read(), watermarks, [name], None)
666
+ self._stream = copy_watermark_widgets(self.read(), watermarks, [name], None)
694
667
  hook_params = obj.hook_params
695
668
 
696
669
  self._init_helper()
@@ -729,7 +702,7 @@ class PdfWrapper:
729
702
 
730
703
  self._key_update_tracker[new_key] = old_key
731
704
  self._stream = update_widget_keys(
732
- self._read(), self.widgets, [old_key], [new_key], [index]
705
+ self.read(), self.widgets, [old_key], [new_key], [index]
733
706
  )
734
707
  self._init_helper()
735
708
 
@@ -754,7 +727,7 @@ class PdfWrapper:
754
727
  indices = [each[2] for each in self._keys_to_update]
755
728
 
756
729
  self._stream = update_widget_keys(
757
- self._read(), self.widgets, old_keys, new_keys, indices
730
+ self.read(), self.widgets, old_keys, new_keys, indices
758
731
  )
759
732
 
760
733
  for each in self._keys_to_update:
@@ -796,7 +769,7 @@ class PdfWrapper:
796
769
  new_widget.font_color = kwargs.get("font_color", DEFAULT_FONT_COLOR)
797
770
 
798
771
  watermarks = create_watermarks_and_draw(
799
- self._read(),
772
+ self.read(),
800
773
  page_number,
801
774
  "text",
802
775
  [
@@ -808,10 +781,10 @@ class PdfWrapper:
808
781
  ],
809
782
  )
810
783
 
811
- stream_with_widgets = self._read()
812
- self._stream = merge_watermarks_with_pdf(self._read(), watermarks)
784
+ stream_with_widgets = self.read()
785
+ self._stream = merge_watermarks_with_pdf(self.read(), watermarks)
813
786
  self._stream = copy_watermark_widgets(
814
- remove_all_widgets(self._read()), stream_with_widgets, None, None
787
+ remove_all_widgets(self.read()), stream_with_widgets, None, None
815
788
  )
816
789
  # because copy_watermark_widgets and remove_all_widgets
817
790
  self._reregister_font()
@@ -850,16 +823,16 @@ class PdfWrapper:
850
823
  image = fp_or_f_obj_or_stream_to_stream(image)
851
824
  image = rotate_image(image, rotation)
852
825
  watermarks = create_watermarks_and_draw(
853
- self._read(),
826
+ self.read(),
854
827
  page_number,
855
828
  "image",
856
829
  [{"stream": image, "x": x, "y": y, "width": width, "height": height}],
857
830
  )
858
831
 
859
- stream_with_widgets = self._read()
860
- self._stream = merge_watermarks_with_pdf(self._read(), watermarks)
832
+ stream_with_widgets = self.read()
833
+ self._stream = merge_watermarks_with_pdf(self.read(), watermarks)
861
834
  self._stream = copy_watermark_widgets(
862
- remove_all_widgets(self._read()), stream_with_widgets, None, None
835
+ remove_all_widgets(self.read()), stream_with_widgets, None, None
863
836
  )
864
837
  # because copy_watermark_widgets and remove_all_widgets
865
838
  self._reregister_font()
@@ -894,7 +867,7 @@ class PdfWrapper:
894
867
  if first_time and getattr(self, "need_appearances"):
895
868
  self.draw_text(" ", 1, 0, 0, font=font_name)
896
869
  self._stream, new_font_name = register_font_acroform(
897
- self._read(), ttf_file, getattr(self, "need_appearances")
870
+ self.read(), ttf_file, getattr(self, "need_appearances")
898
871
  )
899
872
  self._available_fonts[font_name] = new_font_name
900
873
  self._font_register_events.append((font_name, ttf_file))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPDFForm
3
- Version: 3.7.2
3
+ Version: 3.7.4
4
4
  Summary: The Python library for PDF forms.
5
5
  Author: Jinge Li
6
6
  License-Expression: MIT
@@ -79,7 +79,7 @@ A sample PDF form can be found [here](https://chinapandaman.github.io/PyPDFForm/
79
79
  ```python
80
80
  from PyPDFForm import PdfWrapper
81
81
 
82
- filled = PdfWrapper("sample_template.pdf", generate_appearance_streams=True).fill(
82
+ filled = PdfWrapper("sample_template.pdf", need_appearances=True).fill(
83
83
  {
84
84
  "test": "test_1",
85
85
  "check": True,
@@ -37,7 +37,7 @@ A sample PDF form can be found [here](https://chinapandaman.github.io/PyPDFForm/
37
37
  ```python
38
38
  from PyPDFForm import PdfWrapper
39
39
 
40
- filled = PdfWrapper("sample_template.pdf", generate_appearance_streams=True).fill(
40
+ filled = PdfWrapper("sample_template.pdf", need_appearances=True).fill(
41
41
  {
42
42
  "test": "test_1",
43
43
  "check": True,
@@ -121,40 +121,6 @@ def test_issue_613(pdf_samples, request):
121
121
  request.config.results["skip_regenerate"] = len(obj.read()) == len(expected)
122
122
 
123
123
 
124
- @pytest.mark.posix_only
125
- def test_default_font_reportlab_text_alignment(pdf_samples, request):
126
- expected_path = os.path.join(
127
- pdf_samples,
128
- "generate_appearance_streams",
129
- "test_default_font_reportlab_text_alignment.pdf",
130
- )
131
- with open(expected_path, "rb+") as f:
132
- obj = (
133
- PdfWrapper(
134
- os.path.join(pdf_samples, "dummy.pdf"),
135
- generate_appearance_streams=True,
136
- )
137
- .create_field(
138
- Fields.TextField(
139
- "new_text_field_widget",
140
- 1,
141
- 100,
142
- 100,
143
- alignment=1,
144
- )
145
- )
146
- .fill({"new_text_field_widget": "foo"})
147
- )
148
-
149
- request.config.results["expected_path"] = expected_path
150
- request.config.results["stream"] = obj.read()
151
-
152
- expected = f.read()
153
-
154
- assert len(obj.read()) == len(expected)
155
- request.config.results["skip_regenerate"] = len(obj.read()) == len(expected)
156
-
157
-
158
124
  @pytest.mark.posix_only
159
125
  def test_sample_template_library(
160
126
  pdf_samples, image_samples, sample_font_stream, request
@@ -239,7 +205,9 @@ def test_sample_template_library(
239
205
 
240
206
  obj.widgets["new_text_field_widget"].font = "new_font"
241
207
  obj.widgets["new_text_field_widget"].font_color = (1, 0, 0)
242
- obj.widgets["new_text_field_widget"].alignment = 2
208
+ obj.widgets["new_text_field_widget"].alignment = (
209
+ 2 # TODO: why is alignment not rendered right in the appearance stream?
210
+ )
243
211
  obj.widgets["new_checkbox_widget"].size = 40
244
212
  obj.widgets["new_radio_group"].size = 50
245
213
 
@@ -1,182 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- Contains functions for handling and post-processing PDF appearance streams (AP).
4
-
5
- This module is responsible for:
6
- 1. Preparing a PDF form by removing XFA data and setting the /NeedAppearances flag.
7
- 2. Optionally generating appearance streams explicitly using pikepdf.
8
- 3. Post-processing appearance streams, specifically adjusting text alignment for
9
- ReportLab-generated text fields.
10
- """
11
-
12
- from contextlib import suppress
13
- from functools import lru_cache
14
- from io import BytesIO
15
-
16
- from pikepdf import Pdf
17
- from pypdf import PdfReader, PdfWriter
18
- from pypdf.generic import DictionaryObject
19
- from reportlab.pdfbase.pdfmetrics import getFont
20
-
21
- from .constants import (AP, DEFAULT_FONT, FONT_SIZE_IDENTIFIER, XFA, AcroForm,
22
- Annots, BBox, N, Root, Td)
23
- from .middleware.text import Text
24
- from .template import get_widget_key
25
- from .utils import stream_to_io
26
-
27
-
28
- @lru_cache
29
- def appearance_streams_handler(pdf: bytes, generate_appearance_streams: bool) -> bytes:
30
- """
31
- Handles appearance streams and the /NeedAppearances flag for a PDF form.
32
-
33
- This function prepares a PDF for form filling by:
34
- 1. Removing the XFA dictionary if present, as it can interfere with standard
35
- AcroForm processing.
36
- 2. Setting the /NeedAppearances flag in the AcroForm dictionary, which instructs
37
- PDF viewers to generate appearance streams for form fields.
38
- 3. Optionally generating appearance streams explicitly using pikepdf if
39
- `generate_appearance_streams` is True.
40
-
41
- The result is cached using lru_cache for performance.
42
-
43
- Args:
44
- pdf (bytes): The PDF file content as a bytes stream.
45
- generate_appearance_streams (bool): Whether to explicitly generate appearance streams for all form fields.
46
-
47
- Returns:
48
- bytes: The modified PDF content as a bytes stream.
49
- """
50
- reader = PdfReader(stream_to_io(pdf))
51
- writer = PdfWriter()
52
-
53
- if AcroForm in reader.trailer[Root] and XFA in reader.trailer[Root][AcroForm]:
54
- del reader.trailer[Root][AcroForm][XFA]
55
-
56
- writer.append(reader)
57
- writer.set_need_appearances_writer()
58
-
59
- with BytesIO() as f:
60
- writer.write(f)
61
- f.seek(0)
62
- result = f.read()
63
-
64
- if generate_appearance_streams:
65
- with Pdf.open(stream_to_io(result)) as f:
66
- f.generate_appearance_streams()
67
- with BytesIO() as r:
68
- f.save(r)
69
- r.seek(0)
70
- result = r.read()
71
-
72
- return result
73
-
74
-
75
- def appearance_streams_post_processing(
76
- pdf: bytes, widgets: dict, use_full_widget_name: bool, available_fonts: dict
77
- ) -> bytes:
78
- """
79
- Performs post-processing on appearance streams, primarily for text field alignment.
80
-
81
- This function is called after appearance streams have been generated (either by
82
- the PDF viewer or explicitly by pikepdf). It iterates through all annotations
83
- (form fields) and applies necessary adjustments, such as correcting text
84
- alignment for ReportLab-generated appearance streams.
85
-
86
- Args:
87
- pdf (bytes): The PDF file content as a bytes stream.
88
- widgets (dict): A dictionary of `PyPDFForm` widget objects keyed by field name.
89
- use_full_widget_name (bool): Whether full widget names are used.
90
- available_fonts (dict): A dictionary of available fonts for calculating text width.
91
-
92
- Returns:
93
- bytes: The modified PDF content as a bytes stream, or the original stream
94
- if no updates were necessary.
95
- """
96
- reader = PdfReader(stream_to_io(pdf))
97
- writer = PdfWriter()
98
- writer.append(reader)
99
-
100
- needs_update = False
101
- for page in writer.pages:
102
- for annot in page.get(Annots, []):
103
- key = get_widget_key(annot, use_full_widget_name)
104
- widget = widgets[key]
105
-
106
- with suppress(Exception):
107
- needs_update = (
108
- needs_update
109
- or ap_processing_reportlab_text_field_alignment(
110
- annot, widget, available_fonts
111
- )
112
- )
113
-
114
- if not needs_update:
115
- return pdf
116
-
117
- with BytesIO() as f:
118
- writer.write(f)
119
- f.seek(0)
120
- return f.read()
121
-
122
-
123
- def ap_processing_reportlab_text_field_alignment(
124
- annot: DictionaryObject, widget: Text, available_fonts: dict
125
- ) -> bool:
126
- """
127
- Adjusts the text alignment coordinates for text fields whose appearance streams
128
- were generated by ReportLab.
129
-
130
- ReportLab's appearance stream generation may not correctly handle alignment
131
- (center or right alignment). This function calculates the necessary coordinate
132
- shift based on the text width and updates the appearance stream's text
133
- positioning operator (Td).
134
-
135
- Args:
136
- annot (DictionaryObject): The pypdf annotation dictionary object for the field.
137
- widget (Text): The `PyPDFForm` Text widget object containing alignment and value.
138
- available_fonts (dict): A dictionary of available fonts for calculating text width.
139
-
140
- Returns:
141
- bool: True if the appearance stream was modified, False otherwise.
142
- """
143
- if (not widget.alignment) or (not widget.value):
144
- return False
145
-
146
- ap_stream = annot[AP][N].get_data()
147
- bbox = annot[AP][N][BBox]
148
-
149
- # calculate width
150
- font_size = float(
151
- ap_stream.split(bytes(" " + FONT_SIZE_IDENTIFIER, encoding="utf-8"))[0].split(
152
- b" "
153
- )[-1]
154
- )
155
- if widget.font:
156
- width = None
157
- for k, v in available_fonts.items():
158
- if v == widget.font:
159
- width = getFont(k).stringWidth(widget.value, font_size)
160
- break
161
- else:
162
- width = getFont(DEFAULT_FONT).stringWidth(widget.value, font_size)
163
-
164
- # new alignment coordinate stream
165
- alignment_coord = b" ".join(
166
- ap_stream.split(b" " + Td)[0].split(b" ")[-2:] + [Td]
167
- ).split(b"\n")[1]
168
- new_x_coord = (
169
- bbox[2] - bbox[1] - width - float(alignment_coord.split(b" ")[0])
170
- if widget.alignment == 2
171
- else (bbox[2] - bbox[1] - width) / 2
172
- )
173
- new_alignment_coord = b" ".join(
174
- [
175
- bytes(str(new_x_coord), encoding="utf-8"),
176
- ]
177
- + alignment_coord.split(b" ")[1:]
178
- )
179
-
180
- annot[AP][N].set_data(ap_stream.replace(alignment_coord, new_alignment_coord))
181
-
182
- return True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes