python-pdffiller 1.0.0__py3-none-any.whl
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.
- pdffiller/__init__.py +4 -0
- pdffiller/__main__.py +6 -0
- pdffiller/_version.py +6 -0
- pdffiller/cli/__init__.py +0 -0
- pdffiller/cli/args.py +28 -0
- pdffiller/cli/boolean_action.py +35 -0
- pdffiller/cli/cli.py +260 -0
- pdffiller/cli/command.py +291 -0
- pdffiller/cli/commands/__init__.py +0 -0
- pdffiller/cli/commands/dump_data_fields.py +75 -0
- pdffiller/cli/commands/fill_form.py +142 -0
- pdffiller/cli/exit_codes.py +10 -0
- pdffiller/cli/formatters.py +16 -0
- pdffiller/cli/once_argument.py +19 -0
- pdffiller/cli/smart_formatter.py +10 -0
- pdffiller/const.py +22 -0
- pdffiller/exceptions.py +59 -0
- pdffiller/io/__init__.py +0 -0
- pdffiller/io/colors.py +52 -0
- pdffiller/io/output.py +335 -0
- pdffiller/pdf.py +488 -0
- pdffiller/py.typed.py +0 -0
- pdffiller/typing.py +59 -0
- pdffiller/utils.py +36 -0
- pdffiller/widgets/__init__.py +0 -0
- pdffiller/widgets/base.py +107 -0
- pdffiller/widgets/checkbox.py +52 -0
- pdffiller/widgets/radio.py +37 -0
- pdffiller/widgets/text.py +82 -0
- python_pdffiller-1.0.0.dist-info/METADATA +138 -0
- python_pdffiller-1.0.0.dist-info/RECORD +36 -0
- python_pdffiller-1.0.0.dist-info/WHEEL +5 -0
- python_pdffiller-1.0.0.dist-info/entry_points.txt +2 -0
- python_pdffiller-1.0.0.dist-info/licenses/AUTHORS.rst +7 -0
- python_pdffiller-1.0.0.dist-info/licenses/COPYING +22 -0
- python_pdffiller-1.0.0.dist-info/top_level.txt +1 -0
pdffiller/pdf.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
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
|
+
from pypdf import PdfReader, PdfWriter
|
|
16
|
+
from pypdf.errors import PyPdfError
|
|
17
|
+
from pypdf.generic import (
|
|
18
|
+
ArrayObject,
|
|
19
|
+
DictionaryObject,
|
|
20
|
+
NameObject,
|
|
21
|
+
NumberObject,
|
|
22
|
+
TextStringObject,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from pdffiller.io.output import PdfFillerOutput
|
|
26
|
+
|
|
27
|
+
from .typing import (
|
|
28
|
+
Any,
|
|
29
|
+
Callable,
|
|
30
|
+
cast,
|
|
31
|
+
Dict,
|
|
32
|
+
List,
|
|
33
|
+
Optional,
|
|
34
|
+
PathLike,
|
|
35
|
+
StrByteType,
|
|
36
|
+
Type,
|
|
37
|
+
Union,
|
|
38
|
+
)
|
|
39
|
+
from .widgets.base import Widget
|
|
40
|
+
from .widgets.checkbox import CheckBoxWidget
|
|
41
|
+
from .widgets.radio import RadioWidget
|
|
42
|
+
from .widgets.text import TextWidget
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PdfAttributes: # pylint: disable=too-few-public-methods
|
|
46
|
+
"""Various constants, enums, and flags to aid readability."""
|
|
47
|
+
|
|
48
|
+
Widget = "/Widget"
|
|
49
|
+
Subtype = "/Subtype"
|
|
50
|
+
Parent = "/Parent"
|
|
51
|
+
T = "/T"
|
|
52
|
+
V = "/V"
|
|
53
|
+
AS = "/AS"
|
|
54
|
+
Kids = "/Kids"
|
|
55
|
+
AP = "/AP"
|
|
56
|
+
N = "/N"
|
|
57
|
+
D = "/D"
|
|
58
|
+
Opt = "/Opt"
|
|
59
|
+
MaxLen = "/MaxLen"
|
|
60
|
+
AcroForm = "/AcroForm"
|
|
61
|
+
Root = "/Root"
|
|
62
|
+
XFA = "/XFA"
|
|
63
|
+
FT = "/FT"
|
|
64
|
+
Ff = "/Ff"
|
|
65
|
+
Tx = "/Tx"
|
|
66
|
+
Ch = "/Ch"
|
|
67
|
+
Btn = "/Btn"
|
|
68
|
+
Off = "/Off"
|
|
69
|
+
READ_ONLY = 1 << 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Pdf:
|
|
73
|
+
"""
|
|
74
|
+
A class to wrap PDF form operations, providing a simplified interface
|
|
75
|
+
for common tasks such as filling, creating, and manipulating PDF forms.
|
|
76
|
+
|
|
77
|
+
The `Pdf` class encapsulates a PDF document and provides methods
|
|
78
|
+
for interacting with its form fields (widgets) and content.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
TYPE_TO_OBJECT: Dict[str, Type[Widget]] = {
|
|
83
|
+
"text": TextWidget,
|
|
84
|
+
"radio": RadioWidget,
|
|
85
|
+
"checkbox": CheckBoxWidget,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
content: Optional[StrByteType] = None,
|
|
91
|
+
adode_mode: Optional[bool] = True,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Constructor method for the `Pdf` class.
|
|
95
|
+
|
|
96
|
+
Initializes a new `Pdf` object with the given template PDF and optional keyword arguments.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
content (Optional[StrByteType]): The template PDF, provided as either:
|
|
100
|
+
- str: The file path to the PDF.
|
|
101
|
+
- BinaryIO: An open file-like object containing the PDF data.
|
|
102
|
+
adobe_mode (bool): Whether to enable Adobe-specific compatibility mode.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
super().__init__()
|
|
106
|
+
self.widgets: OrderedDict[str, Widget] = OrderedDict()
|
|
107
|
+
self.adobe_mode = adode_mode
|
|
108
|
+
|
|
109
|
+
self._init_helper(content)
|
|
110
|
+
|
|
111
|
+
def _init_helper(self, content: Optional[StrByteType] = None) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Helper method to initialize widgets
|
|
114
|
+
|
|
115
|
+
This method is called during initialization and after certain operations
|
|
116
|
+
that modify the PDF content.
|
|
117
|
+
It rebuilds the widget dictionary.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
content (Optional[StrByteType]): The template PDF, provided as either:
|
|
121
|
+
- str: The file path to the PDF.
|
|
122
|
+
- BinaryIO: An open file-like object containing the PDF data.
|
|
123
|
+
"""
|
|
124
|
+
if not content:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
loaded_widgets: OrderedDict[str, Widget] = OrderedDict()
|
|
128
|
+
try:
|
|
129
|
+
pdf_file = PdfReader(content)
|
|
130
|
+
except PyPdfError as ex:
|
|
131
|
+
PdfFillerOutput().error(str(ex))
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
for i, page in enumerate(pdf_file.pages):
|
|
135
|
+
widgets: Optional[ArrayObject] = page.annotations
|
|
136
|
+
if not widgets:
|
|
137
|
+
continue
|
|
138
|
+
for widget in widgets:
|
|
139
|
+
choices: Optional[List[str]] = None
|
|
140
|
+
if (
|
|
141
|
+
PdfAttributes.Subtype not in widget
|
|
142
|
+
or widget[PdfAttributes.Subtype] != PdfAttributes.Widget
|
|
143
|
+
):
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
if PdfAttributes.T not in widget:
|
|
147
|
+
widget = widget[PdfAttributes.Parent]
|
|
148
|
+
key = self._get_widget_name(widget)
|
|
149
|
+
if not key:
|
|
150
|
+
continue
|
|
151
|
+
widget_type: Optional[str] = self._get_field_type(widget)
|
|
152
|
+
if not widget_type:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
value = widget[PdfAttributes.V] if PdfAttributes.V in widget else None
|
|
156
|
+
if widget_type == "radio":
|
|
157
|
+
if value:
|
|
158
|
+
value = value[1:]
|
|
159
|
+
choices = []
|
|
160
|
+
if PdfAttributes.Kids in widget:
|
|
161
|
+
for each in widget[PdfAttributes.Kids]:
|
|
162
|
+
for each in each[PdfAttributes.AP][PdfAttributes.N].keys():
|
|
163
|
+
if each[1:] not in choices:
|
|
164
|
+
choices.append(each[1:])
|
|
165
|
+
|
|
166
|
+
elif widget_type == "checkbox":
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
PdfAttributes.AP in widget
|
|
170
|
+
and PdfAttributes.N in widget[PdfAttributes.AP]
|
|
171
|
+
and PdfAttributes.D in widget[PdfAttributes.AP]
|
|
172
|
+
and PdfAttributes.AS in widget
|
|
173
|
+
):
|
|
174
|
+
choices = [
|
|
175
|
+
each[1:] for each in (widget[PdfAttributes.AP][PdfAttributes.N]).keys()
|
|
176
|
+
]
|
|
177
|
+
if "Off" not in choices:
|
|
178
|
+
choices.insert(0, "Off")
|
|
179
|
+
elif widget_type in ["list", "combo"] and value:
|
|
180
|
+
choices = [each[1:] for each in widget[PdfAttributes.Opt]]
|
|
181
|
+
if key not in loaded_widgets:
|
|
182
|
+
new_widget = self.TYPE_TO_OBJECT[widget_type](key, i, value)
|
|
183
|
+
if choices and isinstance(new_widget, CheckBoxWidget):
|
|
184
|
+
new_widget.choices = choices
|
|
185
|
+
elif isinstance(new_widget, TextWidget):
|
|
186
|
+
max_length = (
|
|
187
|
+
int(widget[PdfAttributes.MaxLen])
|
|
188
|
+
if PdfAttributes.MaxLen in widget
|
|
189
|
+
else None
|
|
190
|
+
)
|
|
191
|
+
if max_length:
|
|
192
|
+
new_widget.max_length = max_length
|
|
193
|
+
loaded_widgets[key] = new_widget
|
|
194
|
+
else:
|
|
195
|
+
new_widget = loaded_widgets[key]
|
|
196
|
+
if choices and isinstance(new_widget, CheckBoxWidget):
|
|
197
|
+
for each in choices:
|
|
198
|
+
if new_widget.choices is not None:
|
|
199
|
+
if each not in new_widget.choices:
|
|
200
|
+
new_widget.choices.append(each)
|
|
201
|
+
else:
|
|
202
|
+
new_widget.choices = [each]
|
|
203
|
+
|
|
204
|
+
cast(CheckBoxWidget, loaded_widgets[key]).choices = new_widget.choices
|
|
205
|
+
|
|
206
|
+
self.widgets = loaded_widgets
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def schema(self) -> List[Dict[str, Any]]:
|
|
210
|
+
"""
|
|
211
|
+
Returns the JSON schema of the PDF form, describing the structure and data
|
|
212
|
+
types of the form fields.
|
|
213
|
+
|
|
214
|
+
This schema can be used to generate user interfaces or validate data before
|
|
215
|
+
filling the form.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
dict: A dictionary representing the JSON schema of the PDF form.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
return [widget.schema_definition for widget in self.widgets.values()]
|
|
222
|
+
|
|
223
|
+
def fill(
|
|
224
|
+
self,
|
|
225
|
+
input_file: StrByteType,
|
|
226
|
+
output_file: PathLike,
|
|
227
|
+
data: Dict[str, Union[str, int, float, bool]],
|
|
228
|
+
flatten: bool = True,
|
|
229
|
+
) -> "Pdf":
|
|
230
|
+
"""
|
|
231
|
+
Fill the PDF form with data from a dictionary.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
input_file (StrByteType): The template PDF, provided as either:
|
|
235
|
+
- str: The file path to the PDF.
|
|
236
|
+
- BinaryIO: An open file-like object containing the PDF data.
|
|
237
|
+
output_file (PathLike): The output file path.
|
|
238
|
+
data (Dict[str, Union[str, bool, int]]): A dictionary where keys are form field names
|
|
239
|
+
and values are the data to fill the fields with. Values can be strings, booleans,
|
|
240
|
+
or integers.
|
|
241
|
+
flatten (bool): Whether to flatten the form after filling, making the fields read-only
|
|
242
|
+
(default: False).
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Pdf: The `Pdf` object, allowing for method chaining.
|
|
246
|
+
"""
|
|
247
|
+
for key, value in data.items():
|
|
248
|
+
if key in self.widgets:
|
|
249
|
+
self.widgets[key].value = value
|
|
250
|
+
|
|
251
|
+
reader = PdfReader(input_file)
|
|
252
|
+
if self.adobe_mode:
|
|
253
|
+
if (
|
|
254
|
+
PdfAttributes.AcroForm in cast(Any, reader.trailer[PdfAttributes.Root])
|
|
255
|
+
and PdfAttributes.XFA
|
|
256
|
+
in cast(Any, reader.trailer[PdfAttributes.Root])[PdfAttributes.AcroForm]
|
|
257
|
+
):
|
|
258
|
+
del cast(Any, reader.trailer[PdfAttributes.Root])[PdfAttributes.AcroForm][
|
|
259
|
+
PdfAttributes.XFA
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
writer = PdfWriter(reader)
|
|
263
|
+
if self.adobe_mode:
|
|
264
|
+
writer.set_need_appearances_writer()
|
|
265
|
+
|
|
266
|
+
fillers: Dict[str, Callable[[DictionaryObject, Any], None]] = {
|
|
267
|
+
"checkbox": self._fill_checkbox,
|
|
268
|
+
"text": self._fill_text,
|
|
269
|
+
"radio": self._fill_radio,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
output = PdfFillerOutput()
|
|
273
|
+
for page in writer.pages:
|
|
274
|
+
widgets: Optional[ArrayObject] = page.annotations
|
|
275
|
+
if not widgets:
|
|
276
|
+
continue
|
|
277
|
+
for widget in widgets:
|
|
278
|
+
if (
|
|
279
|
+
PdfAttributes.Subtype not in widget
|
|
280
|
+
or widget[PdfAttributes.Subtype] != PdfAttributes.Widget
|
|
281
|
+
):
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
annotation = cast(DictionaryObject, widget.get_object())
|
|
285
|
+
if PdfAttributes.T not in widget:
|
|
286
|
+
widget = widget[PdfAttributes.Parent]
|
|
287
|
+
widget_key: Optional[str] = self._get_widget_name(widget)
|
|
288
|
+
if not widget_key or widget_key not in self.widgets:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
widget_type: Optional[str] = self._get_field_type(widget)
|
|
292
|
+
if not widget_type:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
if widget_type != "radio":
|
|
296
|
+
self.flatten_generic(annotation, flatten)
|
|
297
|
+
else:
|
|
298
|
+
self.flatten_radio(annotation, flatten)
|
|
299
|
+
|
|
300
|
+
if widget_key not in data or widget_type not in fillers:
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
current_widget = self.widgets[widget_key]
|
|
304
|
+
if current_widget.value is None:
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
output.debug(f"Filling {current_widget.name} with {current_widget.value}")
|
|
308
|
+
fillers[widget_type](annotation, current_widget)
|
|
309
|
+
|
|
310
|
+
with open(output_file, "wb") as f:
|
|
311
|
+
writer.write(f)
|
|
312
|
+
|
|
313
|
+
return self
|
|
314
|
+
|
|
315
|
+
def _get_widget_name(self, widget: Any) -> Optional[str]:
|
|
316
|
+
if PdfAttributes.T not in widget:
|
|
317
|
+
return None
|
|
318
|
+
key: Optional[str] = widget[PdfAttributes.T]
|
|
319
|
+
if (
|
|
320
|
+
PdfAttributes.Parent in widget
|
|
321
|
+
and PdfAttributes.T in widget[PdfAttributes.Parent].get_object()
|
|
322
|
+
and widget[PdfAttributes.Parent].get_object()[PdfAttributes.T] != key
|
|
323
|
+
):
|
|
324
|
+
key = f"{self._get_widget_name(widget[PdfAttributes.Parent].get_object())}.{key}"
|
|
325
|
+
|
|
326
|
+
return key
|
|
327
|
+
|
|
328
|
+
def _get_field_type(self, annotation: Any) -> Optional[str]:
|
|
329
|
+
"""
|
|
330
|
+
Determine widget type given its annotations.
|
|
331
|
+
"""
|
|
332
|
+
ft = annotation[PdfAttributes.FT] if PdfAttributes.FT in annotation else None
|
|
333
|
+
ff = annotation[PdfAttributes.Ff] if PdfAttributes.Ff in annotation else None
|
|
334
|
+
|
|
335
|
+
if ft == PdfAttributes.Tx:
|
|
336
|
+
return "text"
|
|
337
|
+
if ft == PdfAttributes.Ch:
|
|
338
|
+
if ff and int(ff) & 1 << 17: # test 18th bit
|
|
339
|
+
return "combo"
|
|
340
|
+
return "list"
|
|
341
|
+
if ft == PdfAttributes.Btn:
|
|
342
|
+
if ff and int(ff) & 1 << 15: # test 16th bit
|
|
343
|
+
return "radio"
|
|
344
|
+
return "checkbox"
|
|
345
|
+
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _fill_text(annotation: DictionaryObject, widget: TextWidget) -> None:
|
|
350
|
+
"""
|
|
351
|
+
Updates the value of a text annotation, setting the text content.
|
|
352
|
+
|
|
353
|
+
This function modifies the value (V) and appearance (AP) of the text
|
|
354
|
+
annotation to reflect the new text content.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
annotation (DictionaryObject): The text annotation dictionary.
|
|
358
|
+
widget (TextWidget): The Text widget object containing the text value.
|
|
359
|
+
"""
|
|
360
|
+
if PdfAttributes.Parent in annotation and PdfAttributes.T not in annotation:
|
|
361
|
+
annotation[NameObject(PdfAttributes.Parent)] = TextStringObject(widget.value)
|
|
362
|
+
annotation[NameObject(PdfAttributes.AP)] = TextStringObject(widget.value)
|
|
363
|
+
else:
|
|
364
|
+
annotation[NameObject(PdfAttributes.V)] = TextStringObject(widget.value)
|
|
365
|
+
annotation[NameObject(PdfAttributes.AP)] = TextStringObject(widget.value)
|
|
366
|
+
|
|
367
|
+
@staticmethod
|
|
368
|
+
def _fill_checkbox(annotation: DictionaryObject, widget: CheckBoxWidget) -> None:
|
|
369
|
+
value: Union[bool, str, None] = widget.value
|
|
370
|
+
if value is None:
|
|
371
|
+
value = False
|
|
372
|
+
if not isinstance(value, bool):
|
|
373
|
+
annotation[NameObject(PdfAttributes.AS)] = NameObject("/" + widget.value)
|
|
374
|
+
annotation[NameObject(PdfAttributes.V)] = NameObject("/" + widget.value)
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
for each in cast(Any, annotation[PdfAttributes.AP])[PdfAttributes.N]:
|
|
378
|
+
if (value and str(each) != PdfAttributes.Off) or (
|
|
379
|
+
not value and str(each) == PdfAttributes.Off
|
|
380
|
+
):
|
|
381
|
+
annotation[NameObject(PdfAttributes.AS)] = NameObject(each)
|
|
382
|
+
annotation[NameObject(PdfAttributes.V)] = NameObject(each)
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
if PdfAttributes.V in annotation:
|
|
386
|
+
del annotation[PdfAttributes.V]
|
|
387
|
+
if PdfAttributes.AS in annotation:
|
|
388
|
+
del annotation[PdfAttributes.AS]
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def _fill_radio(annotation: DictionaryObject, widget: RadioWidget) -> None:
|
|
392
|
+
|
|
393
|
+
for each in (
|
|
394
|
+
cast(Any, annotation[PdfAttributes.Kids])
|
|
395
|
+
if PdfAttributes.T in annotation
|
|
396
|
+
else cast(Any, annotation[PdfAttributes.Parent])[PdfAttributes.Kids]
|
|
397
|
+
):
|
|
398
|
+
# determine the export value of each kid
|
|
399
|
+
keys = each[PdfAttributes.AP][PdfAttributes.N].keys()
|
|
400
|
+
if PdfAttributes.Off in keys:
|
|
401
|
+
keys.remove(PdfAttributes.Off)
|
|
402
|
+
export = list(keys)[0] or None
|
|
403
|
+
|
|
404
|
+
if f"/{widget.value}" == export:
|
|
405
|
+
annotation[NameObject(PdfAttributes.AS)] = NameObject(export)
|
|
406
|
+
cast(Any, annotation[NameObject(PdfAttributes.Parent)])[
|
|
407
|
+
NameObject(PdfAttributes.V)
|
|
408
|
+
] = NameObject(export)
|
|
409
|
+
else:
|
|
410
|
+
annotation[NameObject(PdfAttributes.AS)] = NameObject(PdfAttributes.Off)
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def flatten_radio(annot: DictionaryObject, val: bool) -> None:
|
|
414
|
+
"""
|
|
415
|
+
Flattens a radio button annotation by setting or unsetting the ReadOnly flag,
|
|
416
|
+
making it non-editable or editable based on the `val` parameter.
|
|
417
|
+
|
|
418
|
+
This function modifies the Ff (flags) entry in the radio button's annotation
|
|
419
|
+
dictionary or its parent dictionary if `Parent` exists in `annot`, to set or
|
|
420
|
+
unset the ReadOnly flag, preventing or allowing the user from changing the
|
|
421
|
+
selected option.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
annot (DictionaryObject): The radio button annotation dictionary.
|
|
425
|
+
val (bool): True to flatten (make read-only), False to unflatten (make editable).
|
|
426
|
+
"""
|
|
427
|
+
if PdfAttributes.Parent in annot:
|
|
428
|
+
cast(Any, annot[NameObject(PdfAttributes.Parent)])[NameObject(PdfAttributes.Ff)] = (
|
|
429
|
+
NumberObject(
|
|
430
|
+
(
|
|
431
|
+
int(
|
|
432
|
+
cast(Any, annot[NameObject(PdfAttributes.Parent)]).get(
|
|
433
|
+
NameObject(PdfAttributes.Ff), 0
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
| PdfAttributes.READ_ONLY
|
|
437
|
+
if val
|
|
438
|
+
else int(
|
|
439
|
+
cast(Any, annot[NameObject(PdfAttributes.Parent)]).get(
|
|
440
|
+
NameObject(PdfAttributes.Ff), 0
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
& ~PdfAttributes.READ_ONLY
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
else:
|
|
448
|
+
annot[NameObject(PdfAttributes.Ff)] = NumberObject(
|
|
449
|
+
(
|
|
450
|
+
int(annot.get(NameObject(PdfAttributes.Ff), 0)) | PdfAttributes.READ_ONLY
|
|
451
|
+
if val
|
|
452
|
+
else int(annot.get(NameObject(PdfAttributes.Ff), 0)) & ~PdfAttributes.READ_ONLY
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
@staticmethod
|
|
457
|
+
def flatten_generic(annot: DictionaryObject, val: bool) -> None:
|
|
458
|
+
"""
|
|
459
|
+
Flattens a generic annotation by setting or unsetting the ReadOnly flag,
|
|
460
|
+
making it non-editable or editable based on the `val` parameter.
|
|
461
|
+
|
|
462
|
+
This function modifies the Ff (flags) entry in the annotation dictionary to
|
|
463
|
+
set or unset the ReadOnly flag, preventing or allowing the user from
|
|
464
|
+
interacting with the form field.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
annot (DictionaryObject): The annotation dictionary.
|
|
468
|
+
val (bool): True to flatten (make read-only), False to unflatten (make editable).
|
|
469
|
+
"""
|
|
470
|
+
if PdfAttributes.Parent in annot and PdfAttributes.Ff not in annot:
|
|
471
|
+
cast(Any, annot[NameObject(PdfAttributes.Parent)])[NameObject(PdfAttributes.Ff)] = (
|
|
472
|
+
NumberObject(
|
|
473
|
+
(
|
|
474
|
+
int(annot.get(NameObject(PdfAttributes.Ff), 0)) | PdfAttributes.READ_ONLY
|
|
475
|
+
if val
|
|
476
|
+
else int(annot.get(NameObject(PdfAttributes.Ff), 0))
|
|
477
|
+
& ~PdfAttributes.READ_ONLY
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
)
|
|
481
|
+
else:
|
|
482
|
+
annot[NameObject(PdfAttributes.Ff)] = NumberObject(
|
|
483
|
+
(
|
|
484
|
+
int(annot.get(NameObject(PdfAttributes.Ff), 0)) | PdfAttributes.READ_ONLY
|
|
485
|
+
if val
|
|
486
|
+
else int(annot.get(NameObject(PdfAttributes.Ff), 0)) & ~PdfAttributes.READ_ONLY
|
|
487
|
+
)
|
|
488
|
+
)
|
pdffiller/py.typed.py
ADDED
|
File without changes
|
pdffiller/typing.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Callable,
|
|
6
|
+
cast,
|
|
7
|
+
Dict,
|
|
8
|
+
Generator,
|
|
9
|
+
IO,
|
|
10
|
+
ItemsView,
|
|
11
|
+
List,
|
|
12
|
+
Mapping,
|
|
13
|
+
Optional,
|
|
14
|
+
overload,
|
|
15
|
+
Sequence,
|
|
16
|
+
Tuple,
|
|
17
|
+
Type,
|
|
18
|
+
TYPE_CHECKING,
|
|
19
|
+
TypedDict,
|
|
20
|
+
TypeVar,
|
|
21
|
+
Union,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"cast",
|
|
26
|
+
"overload",
|
|
27
|
+
"Any",
|
|
28
|
+
"Callable",
|
|
29
|
+
"Dict",
|
|
30
|
+
"ExitCode",
|
|
31
|
+
"Generator",
|
|
32
|
+
"IO",
|
|
33
|
+
"ItemsView",
|
|
34
|
+
"List",
|
|
35
|
+
"Optional",
|
|
36
|
+
"Mapping",
|
|
37
|
+
"PathLike",
|
|
38
|
+
"Sequence",
|
|
39
|
+
"Tuple",
|
|
40
|
+
"TypedDict",
|
|
41
|
+
"TypeVar",
|
|
42
|
+
"Type",
|
|
43
|
+
"Union",
|
|
44
|
+
"SubParserType",
|
|
45
|
+
"StrByteType",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
ExitCode = Union[str, int, None]
|
|
50
|
+
PathLike = Union[str, Path]
|
|
51
|
+
|
|
52
|
+
StreamType = IO[Any]
|
|
53
|
+
StrByteType = Union[PathLike, StreamType]
|
|
54
|
+
|
|
55
|
+
if TYPE_CHECKING:
|
|
56
|
+
# pylint: disable=protected-access,line-too-long
|
|
57
|
+
SubParserType = argparse._SubParsersAction["PdfFillerStoreArgumentParser"] # type: ignore[name-defined]
|
|
58
|
+
else:
|
|
59
|
+
SubParserType = Any
|
pdffiller/utils.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pdffiller.typing import Any, Optional, PathLike
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def str_to_path(path: Optional[PathLike]) -> Any:
|
|
8
|
+
"""Convert string or Path to Path
|
|
9
|
+
|
|
10
|
+
:param path: The path to be converted
|
|
11
|
+
:return: The converted path into Path object if successful, else None
|
|
12
|
+
"""
|
|
13
|
+
if not path:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
if not isinstance(path, Path):
|
|
17
|
+
try:
|
|
18
|
+
new_path = Path(str(path))
|
|
19
|
+
except RuntimeError:
|
|
20
|
+
new_path = None
|
|
21
|
+
else:
|
|
22
|
+
new_path = path
|
|
23
|
+
|
|
24
|
+
return new_path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def path_to_str(path: Optional[PathLike]) -> Any:
|
|
28
|
+
"""Convert string or path to string only
|
|
29
|
+
|
|
30
|
+
:param path: The path to be converted
|
|
31
|
+
:return: The converted string from ``path`` if successful, else None
|
|
32
|
+
"""
|
|
33
|
+
if not path:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
return os.fspath(path)
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the base class for all widgets in PyPdfFormFiller.
|
|
3
|
+
|
|
4
|
+
This module defines the base class for form widgets, which are used to
|
|
5
|
+
represent form fields in a PDF document. The Widget class provides
|
|
6
|
+
common attributes and methods for all form widgets, such as name, value,
|
|
7
|
+
and schema definition.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Widget:
|
|
14
|
+
"""
|
|
15
|
+
Base class for form widget.
|
|
16
|
+
|
|
17
|
+
The Widget class provides a base implementation for form widgets,
|
|
18
|
+
which are used to represent form fields in a PDF document. It
|
|
19
|
+
defines common attributes and methods for all form widgets, such
|
|
20
|
+
as name, value, and schema definition.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, name: str, page_number: int, value: Optional[Any] = None) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Initialize a new widget.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name (str): The name of the widget.
|
|
29
|
+
page_number (int): The associated page index
|
|
30
|
+
value (Any): The initial value of the widget. Defaults to None.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__()
|
|
33
|
+
self._name: str = name
|
|
34
|
+
self.page_number: int = page_number
|
|
35
|
+
self._value: Optional[Any] = value
|
|
36
|
+
self._description: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def name(self) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Get the name of the widget.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
str: The name of the widget.
|
|
45
|
+
"""
|
|
46
|
+
return self._name
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def value(self) -> Any:
|
|
50
|
+
"""
|
|
51
|
+
Get the value of the widget.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Any: The value of the widget.
|
|
55
|
+
"""
|
|
56
|
+
return self._value
|
|
57
|
+
|
|
58
|
+
@value.setter
|
|
59
|
+
def value(self, value: Any) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Set the value of the widget.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
value (Any): The value to set.
|
|
65
|
+
"""
|
|
66
|
+
self._value = value
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def description(self) -> Optional[str]:
|
|
70
|
+
"""
|
|
71
|
+
Get the description of the widget.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Any: The description of the widget.
|
|
75
|
+
"""
|
|
76
|
+
return self._description
|
|
77
|
+
|
|
78
|
+
@description.setter
|
|
79
|
+
def description(self, description: Optional[str]) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Set the description of the widget.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
description (str): The description to set.
|
|
85
|
+
"""
|
|
86
|
+
self._description = description
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def schema_definition(self) -> Dict[str, Any]:
|
|
90
|
+
"""
|
|
91
|
+
Get the schema definition of the widget.
|
|
92
|
+
|
|
93
|
+
This method returns a dictionary that defines the schema
|
|
94
|
+
for the widget. The schema definition is used to validate
|
|
95
|
+
the widget's value.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
dict: The schema definition of the widget.
|
|
99
|
+
"""
|
|
100
|
+
result: Dict[str, Any] = {"FieldName": self._name}
|
|
101
|
+
if self._value:
|
|
102
|
+
result["FieldValue"] = self._value
|
|
103
|
+
|
|
104
|
+
if self._description is not None:
|
|
105
|
+
result["Description"] = self._description
|
|
106
|
+
|
|
107
|
+
return result
|