python-pdffiller 1.1.2__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/PKG-INFO +2 -2
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/_version.py +1 -1
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/commands/dump_data_fields.py +2 -1
- python_pdffiller-2.0.0/pdffiller/pdf.py +221 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/typing.py +1 -2
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pyproject.toml +3 -2
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/PKG-INFO +2 -2
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/SOURCES.txt +3 -1
- python_pdffiller-2.0.0/python_pdffiller.egg-info/requires.txt +3 -0
- python_pdffiller-2.0.0/requirements.txt +3 -0
- python_pdffiller-2.0.0/tests/cli/test_dump_data_field.py +106 -0
- python_pdffiller-2.0.0/tests/cli/test_fill_form.py +103 -0
- python_pdffiller-2.0.0/tests/conftest.py +70 -0
- python_pdffiller-2.0.0/tests/unit/__init__.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/unit/test_form_field.py +6 -5
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tox.ini +17 -3
- python_pdffiller-1.1.2/pdffiller/io/custom_pdf_writer.py +0 -143
- python_pdffiller-1.1.2/pdffiller/pdf.py +0 -351
- python_pdffiller-1.1.2/python_pdffiller.egg-info/requires.txt +0 -3
- python_pdffiller-1.1.2/requirements.txt +0 -3
- python_pdffiller-1.1.2/tests/conftest.py +0 -28
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/AUTHORS.rst +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/CHANGELOG.md +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/COPYING +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/MANIFEST.in +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/README.rst +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/Makefile +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/make.bat +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/__init__.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/_static/rtd_literal_block.css +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/_static/rtd_theme_overrides.css +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/_static/terminal_output.css +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/changelog.md +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/cli-commands.rst +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/cli-usage.rst +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/commands/dump_data_fields.rst +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/commands/fill_form.rst +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/conf.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/contributing.rst +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/docs/source/index.rst +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/__init__.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/__main__.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/__init__.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/args.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/boolean_action.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/cli.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/command.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/commands/__init__.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/commands/fill_form.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/exit_codes.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/formatters.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/once_argument.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/smart_formatter.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/const.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/exceptions.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/io/__init__.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/io/colors.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/io/output.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/py.typed.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/utils.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/__init__.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/base.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/checkbox.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/radio.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/widgets/text.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/dependency_links.txt +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/entry_points.txt +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/python_pdffiller.egg-info/top_level.txt +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/requirements-dev.txt +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/requirements-doc.txt +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/requirements-lint.txt +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/requirements-test.txt +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/setup.cfg +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/setup.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/__init__.py +0 -0
- {python_pdffiller-1.1.2/tests/unit → python_pdffiller-2.0.0/tests/cli}/__init__.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/data/empty.pdf +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/data/input.pdf +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/unit/test_formatters.py +0 -0
- {python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/tests/unit/test_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-pdffiller
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Interact with PDF by inspecting or filling it
|
|
5
5
|
Author-email: Jacques Raphanel <jraphanel@sismic.fr>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,7 +26,7 @@ Requires-Python: >=3.9
|
|
|
26
26
|
Description-Content-Type: text/x-rst
|
|
27
27
|
License-File: COPYING
|
|
28
28
|
License-File: AUTHORS.rst
|
|
29
|
-
Requires-Dist:
|
|
29
|
+
Requires-Dist: pymupdf==1.24.10
|
|
30
30
|
Requires-Dist: colorama
|
|
31
31
|
Requires-Dist: pyyaml
|
|
32
32
|
Dynamic: license-file
|
{python_pdffiller-1.1.2 → python_pdffiller-2.0.0}/pdffiller/cli/commands/dump_data_fields.py
RENAMED
|
@@ -30,7 +30,8 @@ def dump_fields_text_formatter(pdf: Pdf) -> None:
|
|
|
30
30
|
|
|
31
31
|
def dump_fields_json_formatter(pdf: Pdf) -> None:
|
|
32
32
|
"""Print output text for dump_fields command as simple text"""
|
|
33
|
-
|
|
33
|
+
if not pdf:
|
|
34
|
+
return
|
|
34
35
|
cli_out_write(json.dumps(pdf.schema, indent=4, ensure_ascii=False))
|
|
35
36
|
|
|
36
37
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A module for wrapping PDF form operations, providing a high-level interface
|
|
3
|
+
for filling, and manipulating PDF forms.
|
|
4
|
+
|
|
5
|
+
This module simplifies common tasks such as:
|
|
6
|
+
- Filling PDF forms with data from a dictionary.
|
|
7
|
+
- Fetching PDF forms fields
|
|
8
|
+
|
|
9
|
+
The core class, `Pdf`, encapsulates a PDF document and provides
|
|
10
|
+
methods for interacting with its form fields and content.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from collections import OrderedDict
|
|
14
|
+
|
|
15
|
+
import pymupdf
|
|
16
|
+
|
|
17
|
+
from pdffiller.exceptions import PdfFillerException
|
|
18
|
+
from pdffiller.io.output import PdfFillerOutput
|
|
19
|
+
|
|
20
|
+
from .typing import Any, cast, Dict, List, Optional, PathLike, StreamType, Type
|
|
21
|
+
from .widgets.base import Widget
|
|
22
|
+
from .widgets.checkbox import CheckBoxWidget
|
|
23
|
+
from .widgets.radio import RadioWidget
|
|
24
|
+
from .widgets.text import TextWidget
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PdfAttributes: # pylint: disable=too-few-public-methods
|
|
28
|
+
"""Various constants, enums, and flags to aid readability."""
|
|
29
|
+
|
|
30
|
+
READ_ONLY = 1 << 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Pdf:
|
|
34
|
+
"""
|
|
35
|
+
A class to wrap PDF form operations, providing a simplified interface
|
|
36
|
+
for common tasks such as filling, creating, and manipulating PDF forms.
|
|
37
|
+
|
|
38
|
+
The `Pdf` class encapsulates a PDF document and provides methods
|
|
39
|
+
for interacting with its form fields (widgets) and content.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
TYPE_TO_OBJECT: Dict[str, Type[Widget]] = {
|
|
44
|
+
"Text": TextWidget,
|
|
45
|
+
"RadioButton": RadioWidget,
|
|
46
|
+
"CheckBox": CheckBoxWidget,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self, filename: Optional[PathLike] = None, stream: Optional[StreamType] = None
|
|
51
|
+
) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Constructor method for the `Pdf` class.
|
|
54
|
+
|
|
55
|
+
Initializes a new `Pdf` object with the given template PDF and optional keyword arguments.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
filename (Optional[PathLike]): Path to the input pdf
|
|
59
|
+
stream (Optional[StreamType]): An open file-like object containing the PDF data.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.widgets: OrderedDict[str, Widget] = OrderedDict()
|
|
64
|
+
self._init_helper(filename, stream)
|
|
65
|
+
|
|
66
|
+
def _init_helper(
|
|
67
|
+
self, filename: Optional[PathLike] = None, stream: Optional[StreamType] = None
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Helper method to initialize widgets
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
filename (Optional[PathLike]): Path to the input pdf
|
|
74
|
+
stream (Optional[StreamType]): An open file-like object containing the PDF data.
|
|
75
|
+
"""
|
|
76
|
+
if not filename and not stream:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
output = PdfFillerOutput()
|
|
80
|
+
output.info("loading file in memory")
|
|
81
|
+
loaded_widgets: OrderedDict[str, Widget] = OrderedDict()
|
|
82
|
+
try:
|
|
83
|
+
doc = pymupdf.open(filename=filename, stream=stream)
|
|
84
|
+
except Exception as ex: # pylint: disable=broad-exception-caught
|
|
85
|
+
PdfFillerOutput().error(str(ex))
|
|
86
|
+
raise PdfFillerException(
|
|
87
|
+
f"failed to load {filename or 'file from input string'}"
|
|
88
|
+
) from ex
|
|
89
|
+
|
|
90
|
+
for i, page in enumerate(doc.pages()):
|
|
91
|
+
output.verbose(f"loading page {i+1}/{doc.page_count}")
|
|
92
|
+
for widget in page.widgets():
|
|
93
|
+
button_states = widget.button_states()
|
|
94
|
+
choices = button_states["normal"] if button_states else None
|
|
95
|
+
|
|
96
|
+
if widget.field_name not in loaded_widgets:
|
|
97
|
+
if widget.field_type_string not in self.TYPE_TO_OBJECT:
|
|
98
|
+
output.verbose(f"unsupported {widget.field_type_string} widget type")
|
|
99
|
+
continue
|
|
100
|
+
new_widget = self.TYPE_TO_OBJECT[widget.field_type_string](
|
|
101
|
+
widget.field_name, i, widget.field_value, widget.field_flags & (1 << 0)
|
|
102
|
+
)
|
|
103
|
+
if choices and isinstance(new_widget, CheckBoxWidget):
|
|
104
|
+
new_widget.choices = choices
|
|
105
|
+
elif isinstance(new_widget, TextWidget):
|
|
106
|
+
new_widget.max_length = widget.text_maxlen
|
|
107
|
+
loaded_widgets[widget.field_name] = new_widget
|
|
108
|
+
else:
|
|
109
|
+
new_widget = loaded_widgets[widget.field_name]
|
|
110
|
+
if choices and isinstance(new_widget, CheckBoxWidget):
|
|
111
|
+
for each in choices:
|
|
112
|
+
if new_widget.choices is not None:
|
|
113
|
+
if each not in new_widget.choices:
|
|
114
|
+
new_widget.choices.append(each)
|
|
115
|
+
else:
|
|
116
|
+
new_widget.choices = [each]
|
|
117
|
+
|
|
118
|
+
cast(CheckBoxWidget, loaded_widgets[widget.field_name]).choices = (
|
|
119
|
+
new_widget.choices
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
self.widgets = loaded_widgets
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def schema(self) -> List[Dict[str, Any]]:
|
|
126
|
+
"""
|
|
127
|
+
Returns the JSON schema of the PDF form, describing the structure and data
|
|
128
|
+
types of the form fields.
|
|
129
|
+
|
|
130
|
+
This schema can be used to generate user interfaces or validate data before
|
|
131
|
+
filling the form.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
dict: A dictionary representing the JSON schema of the PDF form.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
return [widget.schema_definition for widget in self.widgets.values()]
|
|
138
|
+
|
|
139
|
+
def fill(
|
|
140
|
+
self,
|
|
141
|
+
input_file: PathLike,
|
|
142
|
+
output_file: PathLike,
|
|
143
|
+
data: Dict[str, str],
|
|
144
|
+
flatten: bool = True,
|
|
145
|
+
) -> "Pdf":
|
|
146
|
+
"""
|
|
147
|
+
Fill the PDF form with data from a dictionary.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
input_file (PathLike): The input file path.
|
|
151
|
+
output_file (PathLike): The output file path.
|
|
152
|
+
data (Dict[str, Union[str, bool, int]]): A dictionary where keys are form field names
|
|
153
|
+
and values are the data to fill the fields with. Values can be strings, booleans,
|
|
154
|
+
or integers.
|
|
155
|
+
flatten (bool): Whether to flatten the form after filling, making the fields read-only
|
|
156
|
+
(default: False).
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Pdf: The `Pdf` object, allowing for method chaining.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
document = pymupdf.open(filename=input_file)
|
|
163
|
+
except Exception as ex:
|
|
164
|
+
PdfFillerOutput().error(str(ex))
|
|
165
|
+
raise PdfFillerException(f"failed to open {input_file}") from ex
|
|
166
|
+
|
|
167
|
+
output = PdfFillerOutput()
|
|
168
|
+
|
|
169
|
+
output.info("filling pdf with input values")
|
|
170
|
+
# Iterate over all pages and process fields
|
|
171
|
+
for page in document:
|
|
172
|
+
for field in page.widgets():
|
|
173
|
+
if field.field_name in data:
|
|
174
|
+
value = data[field.field_name]
|
|
175
|
+
|
|
176
|
+
# Handling checkboxes
|
|
177
|
+
if (
|
|
178
|
+
field.field_type
|
|
179
|
+
== pymupdf.PDF_WIDGET_TYPE_CHECKBOX # pylint: disable=no-member
|
|
180
|
+
):
|
|
181
|
+
print(f"{field.field_name} => {field.on_state()} vs {value}")
|
|
182
|
+
if value.strip() and "Off" != value.strip():
|
|
183
|
+
output.verbose(
|
|
184
|
+
f"updating checkbox with {value} from {field.field_value}"
|
|
185
|
+
)
|
|
186
|
+
field.field_value = True
|
|
187
|
+
else:
|
|
188
|
+
field.field_value = False
|
|
189
|
+
|
|
190
|
+
# Handling radio buttons
|
|
191
|
+
elif (
|
|
192
|
+
field.field_type
|
|
193
|
+
== pymupdf.PDF_WIDGET_TYPE_RADIOBUTTON # pylint: disable=no-member
|
|
194
|
+
):
|
|
195
|
+
if value == field.on_state():
|
|
196
|
+
output.verbose(
|
|
197
|
+
f"updating radiobutton with {value} from {field.field_value}"
|
|
198
|
+
)
|
|
199
|
+
field.field_value = value
|
|
200
|
+
|
|
201
|
+
# Handling other fields types
|
|
202
|
+
else:
|
|
203
|
+
output.verbose(
|
|
204
|
+
f"updating {field.field_name} with {value} from {field.field_value}"
|
|
205
|
+
)
|
|
206
|
+
field.field_value = value
|
|
207
|
+
|
|
208
|
+
# Update the widget!
|
|
209
|
+
field.update()
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
if flatten:
|
|
213
|
+
output.info("remove all annotations")
|
|
214
|
+
document.bake(annots=False)
|
|
215
|
+
|
|
216
|
+
# Save the modified PDF
|
|
217
|
+
document.save(output_file)
|
|
218
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
219
|
+
output.warning("an error occurs when saving file")
|
|
220
|
+
|
|
221
|
+
return self
|
|
@@ -43,7 +43,7 @@ __all__ = [
|
|
|
43
43
|
"Type",
|
|
44
44
|
"Union",
|
|
45
45
|
"SubParserType",
|
|
46
|
-
"
|
|
46
|
+
"StreamType",
|
|
47
47
|
"TextIO",
|
|
48
48
|
]
|
|
49
49
|
|
|
@@ -52,7 +52,6 @@ ExitCode = Union[str, int, None]
|
|
|
52
52
|
PathLike = Union[str, Path]
|
|
53
53
|
|
|
54
54
|
StreamType = IO[Any]
|
|
55
|
-
StrByteType = Union[PathLike, StreamType]
|
|
56
55
|
|
|
57
56
|
if TYPE_CHECKING:
|
|
58
57
|
# pylint: disable=protected-access,line-too-long
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-pdffiller"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0.0"
|
|
8
8
|
description="Interact with PDF by inspecting or filling it"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
license ="MIT"
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
readme = "README.rst"
|
|
27
27
|
authors = [{ name = "Jacques Raphanel", email = "jraphanel@sismic.fr" }]
|
|
28
28
|
keywords = ["development", "pdf"]
|
|
29
|
-
dependencies = ["
|
|
29
|
+
dependencies = ["pymupdf==1.24.10", "colorama", "pyyaml"]
|
|
30
30
|
|
|
31
31
|
[project.scripts]
|
|
32
32
|
pdffiller = "pdffiller.cli:main"
|
|
@@ -92,6 +92,7 @@ logging_use_named_masks = false
|
|
|
92
92
|
major_on_zero = true
|
|
93
93
|
tag_format = "v{version}"
|
|
94
94
|
build_command = """
|
|
95
|
+
apt update && apt install -y --no-install-recommends pkg-config gcc g++ swig && \
|
|
95
96
|
python -m pip install build -rrequirements.txt && \
|
|
96
97
|
python -m build . && \
|
|
97
98
|
python installer/build.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-pdffiller
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Interact with PDF by inspecting or filling it
|
|
5
5
|
Author-email: Jacques Raphanel <jraphanel@sismic.fr>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,7 +26,7 @@ Requires-Python: >=3.9
|
|
|
26
26
|
Description-Content-Type: text/x-rst
|
|
27
27
|
License-File: COPYING
|
|
28
28
|
License-File: AUTHORS.rst
|
|
29
|
-
Requires-Dist:
|
|
29
|
+
Requires-Dist: pymupdf==1.24.10
|
|
30
30
|
Requires-Dist: colorama
|
|
31
31
|
Requires-Dist: pyyaml
|
|
32
32
|
Dynamic: license-file
|
|
@@ -48,7 +48,6 @@ pdffiller/cli/commands/dump_data_fields.py
|
|
|
48
48
|
pdffiller/cli/commands/fill_form.py
|
|
49
49
|
pdffiller/io/__init__.py
|
|
50
50
|
pdffiller/io/colors.py
|
|
51
|
-
pdffiller/io/custom_pdf_writer.py
|
|
52
51
|
pdffiller/io/output.py
|
|
53
52
|
pdffiller/widgets/__init__.py
|
|
54
53
|
pdffiller/widgets/base.py
|
|
@@ -63,6 +62,9 @@ python_pdffiller.egg-info/requires.txt
|
|
|
63
62
|
python_pdffiller.egg-info/top_level.txt
|
|
64
63
|
tests/__init__.py
|
|
65
64
|
tests/conftest.py
|
|
65
|
+
tests/cli/__init__.py
|
|
66
|
+
tests/cli/test_dump_data_field.py
|
|
67
|
+
tests/cli/test_fill_form.py
|
|
66
68
|
tests/data/empty.pdf
|
|
67
69
|
tests/data/input.pdf
|
|
68
70
|
tests/unit/__init__.py
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from pdffiller.cli import cli
|
|
7
|
+
from pdffiller.cli.exit_codes import (
|
|
8
|
+
ERROR_COMMAND_NAME,
|
|
9
|
+
ERROR_UNEXPECTED,
|
|
10
|
+
SUCCESS,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_incomplete_no_action():
|
|
15
|
+
"""test empty command-line"""
|
|
16
|
+
|
|
17
|
+
with mock.patch("sys.argv", []):
|
|
18
|
+
assert cli.main() == ERROR_COMMAND_NAME
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.parametrize("argv", [])
|
|
22
|
+
def test_incomplete(argv):
|
|
23
|
+
"""test empty command-line"""
|
|
24
|
+
|
|
25
|
+
# Test through direct command-line
|
|
26
|
+
with mock.patch("sys.argv", ["pdffiller", "dump_data_fields"] + argv):
|
|
27
|
+
assert cli.main() == ERROR_UNEXPECTED
|
|
28
|
+
|
|
29
|
+
# Test with direct call to main function
|
|
30
|
+
assert cli.main(["dump_data_fields"] + argv) == ERROR_UNEXPECTED
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_complete(test_data_dir, capsys):
|
|
34
|
+
"""test complete command-line"""
|
|
35
|
+
|
|
36
|
+
argv = [
|
|
37
|
+
str(test_data_dir / "input.pdf"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
json_fields = """[
|
|
41
|
+
{
|
|
42
|
+
"FieldType": "text",
|
|
43
|
+
"FieldName": "Lastname"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"FieldType": "text",
|
|
47
|
+
"FieldName": "Firstname"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"FieldType": "checkbox",
|
|
51
|
+
"FieldOptions": [
|
|
52
|
+
"Off",
|
|
53
|
+
"Oui"
|
|
54
|
+
],
|
|
55
|
+
"FieldName": "Men"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"FieldType": "checkbox",
|
|
59
|
+
"FieldOptions": [
|
|
60
|
+
"Off",
|
|
61
|
+
"Oui"
|
|
62
|
+
],
|
|
63
|
+
"FieldName": "Women"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"FieldType": "radio",
|
|
67
|
+
"FieldOptions": [
|
|
68
|
+
"Off",
|
|
69
|
+
"Single",
|
|
70
|
+
"Married",
|
|
71
|
+
"Divorced"
|
|
72
|
+
],
|
|
73
|
+
"FieldName": "MaritalStatus",
|
|
74
|
+
"FieldValue": "Off"
|
|
75
|
+
}
|
|
76
|
+
]"""
|
|
77
|
+
# Test through direct command-line
|
|
78
|
+
with mock.patch(
|
|
79
|
+
"sys.argv",
|
|
80
|
+
["pdffiller", "dump_data_fields", "-fjson"] + argv,
|
|
81
|
+
):
|
|
82
|
+
assert cli.main() == SUCCESS
|
|
83
|
+
out, err = capsys.readouterr()
|
|
84
|
+
assert "loading file in memory" == err.strip()
|
|
85
|
+
assert json.loads(json_fields) == json.loads(out)
|
|
86
|
+
|
|
87
|
+
# Test with direct call to main function
|
|
88
|
+
assert cli.main(["dump_data_fields"] + argv) == SUCCESS
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_complete_with_invalid_file(test_data_dir):
|
|
92
|
+
"""test complete command-line"""
|
|
93
|
+
|
|
94
|
+
argv = [
|
|
95
|
+
str(test_data_dir / "empty.pdf"),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
# Test through direct command-line
|
|
99
|
+
with mock.patch(
|
|
100
|
+
"sys.argv",
|
|
101
|
+
["pdffiller", "dump_data_fields"] + argv,
|
|
102
|
+
):
|
|
103
|
+
assert cli.main() == ERROR_UNEXPECTED
|
|
104
|
+
|
|
105
|
+
# Test with direct call to main function
|
|
106
|
+
assert cli.main(["dump_data_fields"] + argv) == ERROR_UNEXPECTED
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from pdffiller.cli import cli
|
|
7
|
+
from pdffiller.cli.exit_codes import (
|
|
8
|
+
ERROR_COMMAND_NAME,
|
|
9
|
+
ERROR_UNEXPECTED,
|
|
10
|
+
SUCCESS,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_incomplete_no_action():
|
|
15
|
+
"""test empty command-line"""
|
|
16
|
+
|
|
17
|
+
with mock.patch("sys.argv", []):
|
|
18
|
+
assert cli.main() == ERROR_COMMAND_NAME
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.parametrize("argv", [])
|
|
22
|
+
def test_incomplete(argv):
|
|
23
|
+
"""test empty command-line"""
|
|
24
|
+
|
|
25
|
+
# Test through direct command-line
|
|
26
|
+
with mock.patch("sys.argv", ["pdffiller", "fill_form"] + argv):
|
|
27
|
+
assert cli.main() == ERROR_UNEXPECTED
|
|
28
|
+
|
|
29
|
+
# Test with direct call to main function
|
|
30
|
+
assert cli.main(["fill_form"] + argv) == ERROR_UNEXPECTED
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_complete_with_files_only(test_data_dir, input_json_fields, output_pdf_path):
|
|
34
|
+
"""test complete command-line"""
|
|
35
|
+
|
|
36
|
+
argv = [
|
|
37
|
+
"-d",
|
|
38
|
+
str(input_json_fields),
|
|
39
|
+
"-o",
|
|
40
|
+
str(output_pdf_path),
|
|
41
|
+
str(test_data_dir / "input.pdf"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Test through direct command-line
|
|
45
|
+
with mock.patch(
|
|
46
|
+
"sys.argv",
|
|
47
|
+
["pdffiller", "fill_form"] + argv,
|
|
48
|
+
):
|
|
49
|
+
assert cli.main() == SUCCESS
|
|
50
|
+
assert output_pdf_path.exists()
|
|
51
|
+
output_pdf_path.unlink()
|
|
52
|
+
|
|
53
|
+
# Test with direct call to main function
|
|
54
|
+
assert cli.main(["fill_form"] + argv) == SUCCESS
|
|
55
|
+
assert output_pdf_path.exists()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_complete_with_invalid_file(test_data_dir, input_json_fields, output_pdf_path):
|
|
59
|
+
"""test complete command-line"""
|
|
60
|
+
|
|
61
|
+
argv = [
|
|
62
|
+
"-d",
|
|
63
|
+
input_json_fields,
|
|
64
|
+
"-o",
|
|
65
|
+
output_pdf_path,
|
|
66
|
+
str(test_data_dir / "empty.pdf"),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Test through direct command-line
|
|
70
|
+
with mock.patch(
|
|
71
|
+
"sys.argv",
|
|
72
|
+
["pdffiller", "fill_form"] + argv,
|
|
73
|
+
):
|
|
74
|
+
assert cli.main() == ERROR_UNEXPECTED
|
|
75
|
+
|
|
76
|
+
# Test with direct call to main function
|
|
77
|
+
assert cli.main(["fill_form"] + argv) == ERROR_UNEXPECTED
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_complete_with_file_and_string(test_data_dir, input_json_fields, output_pdf_path):
|
|
81
|
+
"""test complete command-line"""
|
|
82
|
+
|
|
83
|
+
json_fields = input_json_fields.read_text()
|
|
84
|
+
argv = [
|
|
85
|
+
"-i",
|
|
86
|
+
json_fields,
|
|
87
|
+
"-o",
|
|
88
|
+
str(output_pdf_path),
|
|
89
|
+
str(test_data_dir / "input.pdf"),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
# Test through direct command-line
|
|
93
|
+
with mock.patch(
|
|
94
|
+
"sys.argv",
|
|
95
|
+
["pdffiller", "fill_form"] + argv,
|
|
96
|
+
):
|
|
97
|
+
assert cli.main() == SUCCESS
|
|
98
|
+
assert output_pdf_path.exists()
|
|
99
|
+
output_pdf_path.unlink()
|
|
100
|
+
|
|
101
|
+
# Test with direct call to main function
|
|
102
|
+
assert cli.main(["fill_form"] + argv) == SUCCESS
|
|
103
|
+
assert output_pdf_path.exists()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from pdffiller.typing import Generator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture(scope="session")
|
|
11
|
+
def test_dir(pytestconfig) -> Path:
|
|
12
|
+
"""Retrieve path to tests directory from test environment"""
|
|
13
|
+
return pytestconfig.rootdir / "tests"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(scope="session")
|
|
17
|
+
def test_data_dir(pytestconfig) -> Path:
|
|
18
|
+
"""Retrieve path to tests data directory from test environment"""
|
|
19
|
+
return pytestconfig.rootdir / "tests" / "data"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(name="tmp_output_dir")
|
|
23
|
+
def fixture_tmp_output_dir(tmp_path) -> Generator[Path, None, None]:
|
|
24
|
+
"""Generate temporary history file"""
|
|
25
|
+
local_output_dir = tmp_path / "_output"
|
|
26
|
+
local_output_dir.mkdir(parents=True)
|
|
27
|
+
yield local_output_dir
|
|
28
|
+
shutil.rmtree(local_output_dir)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(name="input_json_fields")
|
|
32
|
+
def fixture_dynamic_config_file(tmp_path) -> Generator[Path, None, None]:
|
|
33
|
+
"""Generate temporary input json file"""
|
|
34
|
+
config_path = tmp_path / "input.json"
|
|
35
|
+
|
|
36
|
+
json_content = """[
|
|
37
|
+
{
|
|
38
|
+
"name": "Lastname",
|
|
39
|
+
"value": "Doe"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "Firstname",
|
|
43
|
+
"value": "John"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "Men",
|
|
47
|
+
"value": "On"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "Women",
|
|
51
|
+
"value": ""
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "MaritalStatus",
|
|
55
|
+
"value": "Single"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
"""
|
|
59
|
+
config_path.write_text(json_content)
|
|
60
|
+
yield config_path
|
|
61
|
+
config_path.unlink()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.fixture(name="output_pdf_path")
|
|
65
|
+
def fixture_output_pdf_path(tmp_path) -> Generator[Path, None, None]:
|
|
66
|
+
"""Generate temporary output pdf file"""
|
|
67
|
+
output_path = tmp_path / f"{time.time()}.pdf"
|
|
68
|
+
yield output_path
|
|
69
|
+
if output_path.exists():
|
|
70
|
+
output_path.unlink()
|
|
File without changes
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
3
|
from pdffiller.pdf import Pdf
|
|
4
|
+
from pdffiller.exceptions import PdfFillerException
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def test_valid_pdf(test_data_dir):
|
|
@@ -14,11 +15,11 @@ def test_valid_pdf(test_data_dir):
|
|
|
14
15
|
assert schema[3]["FieldName"] == "Women"
|
|
15
16
|
assert schema[4]["FieldName"] == "MaritalStatus"
|
|
16
17
|
assert len(schema[4]["FieldOptions"]) == 4
|
|
17
|
-
assert schema[4]["FieldOptions"]
|
|
18
|
-
assert schema[4]["FieldOptions"]
|
|
19
|
-
assert schema[4]["FieldValue"] == "
|
|
18
|
+
assert "Divorced" in schema[4]["FieldOptions"]
|
|
19
|
+
assert "Off" in schema[4]["FieldOptions"]
|
|
20
|
+
assert schema[4]["FieldValue"] == "Off"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def test_invalid_pdf(test_data_dir):
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
with pytest.raises(PdfFillerException):
|
|
25
|
+
Pdf(str(test_data_dir / "empty.pdf"))
|