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.
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PKG-INFO +2 -2
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/__init__.py +1 -1
- pypdfform-3.7.4/PyPDFForm/ap.py +64 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/constants.py +0 -4
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/wrapper.py +36 -63
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/PKG-INFO +2 -2
- {pypdfform-3.7.2 → pypdfform-3.7.4}/README.md +1 -1
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_generate_appearance_streams.py +3 -35
- pypdfform-3.7.2/PyPDFForm/ap.py +0 -182
- {pypdfform-3.7.2 → pypdfform-3.7.4}/LICENSE +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/adapter.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/coordinate.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/deprecation.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/filler.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/font.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/hooks.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/image.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/__init__.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/base.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/checkbox.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/dropdown.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/image.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/radio.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/signature.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/middleware/text.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/patterns.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/template.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/utils.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/watermark.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/__init__.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/base.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/bedrock.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/checkbox.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/dropdown.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/image.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/radio.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/signature.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm/widgets/text.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/SOURCES.txt +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/requires.txt +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/pyproject.toml +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/setup.cfg +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_create_widget.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_dropdown.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_extract_values.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_fill_method.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_font_widths.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_functional.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_need_appearances.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_paragraph.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_signature.py +0 -0
- {pypdfform-3.7.2 → pypdfform-3.7.4}/tests/test_use_full_widget_name.py +0 -0
- {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.
|
|
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",
|
|
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.
|
|
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
|
|
@@ -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
|
|
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.
|
|
144
|
+
if not self.read():
|
|
145
145
|
return other
|
|
146
146
|
|
|
147
|
-
if not other.
|
|
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.
|
|
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.
|
|
180
|
+
self.read(),
|
|
181
181
|
getattr(self, "use_full_widget_name"),
|
|
182
182
|
)
|
|
183
|
-
if self.
|
|
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.
|
|
204
|
-
self._available_fonts.update(**get_all_available_fonts(self.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
327
|
+
Reads the PDF content from the underlying stream.
|
|
328
328
|
|
|
329
|
-
This method
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
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.
|
|
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.
|
|
411
|
+
stream_with_widgets = self.read()
|
|
439
412
|
self._stream = copy_watermark_widgets(
|
|
440
413
|
generate_coordinate_grid(
|
|
441
|
-
remove_all_widgets(self.
|
|
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.
|
|
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.
|
|
557
|
+
watermarks = getattr(widget_class, "bulk_watermarks")(widgets, self.read())
|
|
585
558
|
self._stream = copy_watermark_widgets(
|
|
586
|
-
self.
|
|
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.
|
|
664
|
+
watermarks = obj.watermarks(self.read())
|
|
692
665
|
|
|
693
|
-
self._stream = copy_watermark_widgets(self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
812
|
-
self._stream = merge_watermarks_with_pdf(self.
|
|
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.
|
|
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.
|
|
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.
|
|
860
|
-
self._stream = merge_watermarks_with_pdf(self.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
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",
|
|
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 =
|
|
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
|
|
pypdfform-3.7.2/PyPDFForm/ap.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|