python-pdffiller 1.1.0__py3-none-any.whl → 1.1.1__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/_version.py CHANGED
@@ -3,4 +3,4 @@ __copyright__ = "Copyright 2025 SISMIC"
3
3
  __email__ = "jraphanel@sismic.fr"
4
4
  __license__ = "MIT"
5
5
  __title__ = "pdffiller"
6
- __version__ = "1.1.0"
6
+ __version__ = "1.1.1"
@@ -131,6 +131,9 @@ def fill_form(parser: PdfFillerArgumentParser, *args: Any) -> Any:
131
131
  input_dict[html.unescape(field["name"])] = html.unescape(field["value"])
132
132
  input_data = input_dict
133
133
 
134
+ output.info(f"input file: {opts.file}")
135
+ output.info("input values are:")
136
+ output.info(json.dumps(input_data, indent=4))
134
137
  try:
135
138
  pdf = Pdf()
136
139
  pdf.fill(opts.file, opts.output, input_data, opts.flatten)
@@ -0,0 +1,143 @@
1
+ from pypdf import PageObject, PdfWriter
2
+ from pypdf.constants import AnnotationDictionaryAttributes as AA
3
+ from pypdf.constants import CatalogDictionary
4
+ from pypdf.constants import FieldDictionaryAttributes as FA
5
+ from pypdf.constants import InteractiveFormDictEntries
6
+ from pypdf.constants import PageAttributes as PG
7
+ from pypdf.errors import PyPdfError
8
+ from pypdf.generic import (
9
+ ArrayObject,
10
+ DictionaryObject,
11
+ NameObject,
12
+ NumberObject,
13
+ RectangleObject,
14
+ TextStringObject,
15
+ )
16
+
17
+ from ..typing import cast, Dict, List, Optional, Tuple, Union
18
+ from .output import PdfFillerOutput
19
+
20
+
21
+ class CustomPdfWriter(PdfWriter): # pylint: disable=abstract-method
22
+ """Overwrite PdfWriter to by-pass the related to flatten mode."""
23
+
24
+ def update_field_values(
25
+ self,
26
+ page: Union[PageObject, List[PageObject], None],
27
+ fields: Dict[str, Union[str, List[str], Tuple[str, str, float]]],
28
+ flags: FA.FfBits = PdfWriter.FFBITS_NUL,
29
+ auto_regenerate: Optional[bool] = True,
30
+ flatten: bool = False,
31
+ ) -> None:
32
+ """
33
+ Update the form field values for a given page from a fields dictionary.
34
+
35
+ Copy field texts and values from fields to page.
36
+ If the field links to a parent object, add the information to the parent.
37
+
38
+ Args:
39
+ page: `PageObject` - references **PDF writer's page** where the
40
+ annotations and field data will be updated.
41
+ `List[Pageobject]` - provides list of pages to be processed.
42
+ `None` - all pages.
43
+ fields: a Python dictionary of:
44
+
45
+ * field names (/T) as keys and text values (/V) as value
46
+ * field names (/T) as keys and list of text values (/V) for multiple choice list
47
+ * field names (/T) as keys and tuple of:
48
+ * text values (/V)
49
+ * font id (e.g. /F1, the font id must exist)
50
+ * font size (0 for autosize)
51
+
52
+ flags: A set of flags from :class:`~pypdf.constants.FieldDictionaryAttributes.FfBits`.
53
+
54
+ auto_regenerate: Set/unset the need_appearances flag;
55
+ the flag is unchanged if auto_regenerate is None.
56
+
57
+ flatten: Whether or not to flatten the annotation. If True, this adds the annotation's
58
+ appearance stream to the page contents. Note that this option does not remove the
59
+ annotation itself.
60
+
61
+ """
62
+ if CatalogDictionary.ACRO_FORM not in self._root_object:
63
+ raise PyPdfError("No /AcroForm dictionary in PDF of PdfWriter Object")
64
+ af = cast(DictionaryObject, self._root_object[CatalogDictionary.ACRO_FORM])
65
+ if InteractiveFormDictEntries.Fields not in af:
66
+ raise PyPdfError("No /Fields dictionary in PDF of PdfWriter Object")
67
+ if isinstance(auto_regenerate, bool):
68
+ self.set_need_appearances_writer(auto_regenerate)
69
+ # Iterate through pages, update field values
70
+ if page is None:
71
+ page = list(self.pages)
72
+ if isinstance(page, list):
73
+ for p in page:
74
+ if PG.ANNOTS in p: # just to prevent warnings
75
+ self.update_field_values(p, fields, flags, None, flatten=flatten)
76
+ return
77
+ output = PdfFillerOutput()
78
+ if PG.ANNOTS not in page:
79
+ output.warning(f"No fields to update on this page {__name__}")
80
+ return
81
+ for annotation in page[PG.ANNOTS]: # type: ignore
82
+ annotation = cast(DictionaryObject, annotation.get_object())
83
+ if annotation.get("/Subtype", "") != "/Widget":
84
+ continue
85
+ if "/FT" in annotation and "/T" in annotation:
86
+ parent_annotation = annotation
87
+ else:
88
+ parent_annotation = annotation.get(PG.PARENT, DictionaryObject()).get_object()
89
+
90
+ for field, value in fields.items():
91
+ if not (
92
+ self._get_qualified_field_name(parent_annotation) == field
93
+ or parent_annotation.get("/T", None) == field
94
+ ):
95
+ continue
96
+ if parent_annotation.get("/FT", None) == "/Ch" and "/I" in parent_annotation:
97
+ del parent_annotation["/I"]
98
+ if flags:
99
+ annotation[NameObject(FA.Ff)] = NumberObject(flags)
100
+ if not (
101
+ value is None and flatten
102
+ ): # Only change values if given by user and not flattening.
103
+ if isinstance(value, list):
104
+ lst = ArrayObject(TextStringObject(v) for v in value)
105
+ parent_annotation[NameObject(FA.V)] = lst
106
+ elif isinstance(value, tuple):
107
+ annotation[NameObject(FA.V)] = TextStringObject(
108
+ value[0],
109
+ )
110
+ else:
111
+ parent_annotation[NameObject(FA.V)] = TextStringObject(value)
112
+ if parent_annotation.get(FA.FT) == "/Btn":
113
+ # Checkbox button (no /FT found in Radio widgets)
114
+ v = NameObject(value)
115
+ ap = cast(DictionaryObject, annotation[NameObject(AA.AP)])
116
+ normal_ap = cast(DictionaryObject, ap["/N"])
117
+ exist = True
118
+ if v not in normal_ap:
119
+ v = NameObject("/Off")
120
+ exist = False
121
+ appearance_stream_obj = normal_ap.get(v)
122
+ # other cases will be updated through the for loop
123
+ annotation[NameObject(AA.AS)] = v
124
+ annotation[NameObject(FA.V)] = v
125
+ if flatten and appearance_stream_obj is not None and exist:
126
+ # We basically copy the entire appearance stream, which should be
127
+ # an XObject that is already registered. No need to add font resources.
128
+ rct = cast(RectangleObject, annotation[AA.Rect])
129
+ self._add_apstream_object(
130
+ page, appearance_stream_obj, field, rct[0], rct[1]
131
+ )
132
+ elif parent_annotation.get(FA.FT) == "/Tx" or parent_annotation.get(FA.FT) == "/Ch":
133
+ # textbox
134
+ if isinstance(value, tuple):
135
+ self._update_field_annotation(
136
+ page, parent_annotation, annotation, value[1], value[2], flatten=flatten
137
+ )
138
+ else:
139
+ self._update_field_annotation(
140
+ page, parent_annotation, annotation, flatten=flatten
141
+ )
142
+ elif annotation.get(FA.FT) == "/Sig": # deprecated # not implemented yet
143
+ output.warning(f"Signature forms not implemented yet {__name__}")
pdffiller/io/output.py CHANGED
@@ -7,7 +7,7 @@ from colorama import Fore, Style
7
7
  from pdffiller import const
8
8
  from pdffiller.exceptions import CommandLineError
9
9
  from pdffiller.io.colors import color_enabled, is_terminal
10
- from pdffiller.typing import Dict, Optional, Union
10
+ from pdffiller.typing import Dict, Optional, TextIO, Union
11
11
 
12
12
  LEVEL_QUIET = 80 # -q
13
13
  LEVEL_ERROR = 70 # Errors
@@ -56,14 +56,11 @@ class PdfFillerOutput:
56
56
  # Singleton
57
57
  _output_level: int = LEVEL_STATUS
58
58
  _output_file: Optional[str] = None
59
+ _output_stream: Optional[TextIO] = None
59
60
 
60
61
  def __init__(self, scope: str = "") -> None:
61
- self.stream = sys.stderr
62
+ self.stream = self._output_stream or sys.stderr
62
63
  self._scope = scope
63
- if self._output_file:
64
- self.stream = open( # pylint: disable=consider-using-with
65
- self._output_file, "wt", encoding="utf-8"
66
- )
67
64
  self._color: bool = color_enabled(self.stream)
68
65
 
69
66
  @classmethod
@@ -104,6 +101,10 @@ class PdfFillerOutput:
104
101
  :param file_path: Optional path to output log file.
105
102
  """
106
103
  cls._output_file = file_path
104
+ if cls._output_file:
105
+ cls._output_stream = open( # pylint: disable=consider-using-with
106
+ cls._output_file, "wt", encoding="utf-8"
107
+ )
107
108
 
108
109
  @classmethod
109
110
  def level_allowed(cls, level: int) -> bool:
pdffiller/pdf.py CHANGED
@@ -12,10 +12,11 @@ methods for interacting with its form fields and content.
12
12
 
13
13
  from collections import OrderedDict
14
14
 
15
- from pypdf import PdfReader, PdfWriter
15
+ from pypdf import PdfReader
16
16
  from pypdf.errors import PyPdfError
17
17
  from pypdf.generic import ArrayObject, DictionaryObject, NameObject
18
18
 
19
+ from pdffiller.io.custom_pdf_writer import CustomPdfWriter
19
20
  from pdffiller.io.output import PdfFillerOutput
20
21
 
21
22
  from .typing import (
@@ -118,6 +119,8 @@ class Pdf:
118
119
  if not content:
119
120
  return
120
121
 
122
+ output = PdfFillerOutput()
123
+ output.verbose("loading file in memory")
121
124
  loaded_widgets: OrderedDict[str, Widget] = OrderedDict()
122
125
  try:
123
126
  pdf_file = PdfReader(content)
@@ -126,6 +129,7 @@ class Pdf:
126
129
  return
127
130
 
128
131
  for i, page in enumerate(pdf_file.pages):
132
+ output.verbose(f"loading page {i+1}/{len(pdf_file.pages)}")
129
133
  widgets: Optional[ArrayObject] = page.annotations
130
134
  if not widgets:
131
135
  continue
@@ -241,23 +245,30 @@ class Pdf:
241
245
  Pdf: The `Pdf` object, allowing for method chaining.
242
246
  """
243
247
  reader = PdfReader(input_file)
248
+ output = PdfFillerOutput()
244
249
 
245
250
  self._init_helper(input_file)
246
251
  fields: Dict[str, Union[str, List[str], Tuple[str, str, float]]] = {}
247
252
 
253
+ output.verbose("checking value for radio/checkbox ...")
248
254
  for name, value in data.items():
249
255
  widget = self.widgets.get(name)
250
256
  fields[name] = value
251
257
  if isinstance(widget, CheckBoxWidget):
252
258
  if value and value[0] != "/":
253
- fields[name] = "/" + value
259
+ output.info(f"override {name} value with /{value}")
260
+ fields[name] = f"/{value}"
254
261
 
255
- writer = PdfWriter(reader)
256
- writer.update_page_form_field_values(None, fields, auto_regenerate=False, flatten=flatten)
262
+ output.info("fill pdf with input values")
263
+ writer = CustomPdfWriter(reader)
264
+ writer.update_field_values(None, fields, auto_regenerate=False, flatten=flatten)
257
265
  if flatten:
266
+ output.info("remove all annotations")
258
267
  writer.remove_annotations(None)
268
+ output.info("compress file")
259
269
  writer.compress_identical_objects(remove_identicals=True, remove_orphans=True)
260
270
 
271
+ output.info(f"write {output_file} on the disk")
261
272
  with open(output_file, "wb") as f:
262
273
  writer.write(f)
263
274
 
pdffiller/typing.py CHANGED
@@ -13,6 +13,7 @@ from typing import (
13
13
  Optional,
14
14
  overload,
15
15
  Sequence,
16
+ TextIO,
16
17
  Tuple,
17
18
  Type,
18
19
  TYPE_CHECKING,
@@ -43,6 +44,7 @@ __all__ = [
43
44
  "Union",
44
45
  "SubParserType",
45
46
  "StrByteType",
47
+ "TextIO",
46
48
  ]
47
49
 
48
50
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-pdffiller
3
- Version: 1.1.0
3
+ Version: 1.1.1
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
@@ -1,11 +1,11 @@
1
1
  pdffiller/__init__.py,sha256=0HtgXhEV1fKTKAcOXGcq4UsCFflDIPCDQvckshZf-1k,195
2
2
  pdffiller/__main__.py,sha256=7NPQgZVx6VSZS7OrmyJQ_O1vL4wiSqhiILi-outwUqM,107
3
- pdffiller/_version.py,sha256=jkVj4mFxUGMPJpsXS5J_l0Rayrz_hST2H3Fewc4-x-A,172
3
+ pdffiller/_version.py,sha256=vef-epYlxNmC71LS6tXOTkDXOOxxiF0dTMJwJJdSnOM,172
4
4
  pdffiller/const.py,sha256=if_j5I8ftczpjrzZjA7idv-XpvIj1-XBF4oe6VtQvF0,434
5
5
  pdffiller/exceptions.py,sha256=CdN93bZ0mBBS5vLxg14FYZUy4xkYqoD3_SzqtSkZr4g,1624
6
- pdffiller/pdf.py,sha256=kfi1tvUHI3zQJPBe5uSapwFfjI9cnNOGyLaoXQE0QXc,11723
6
+ pdffiller/pdf.py,sha256=VpixUDvzgXYxNX6A5LkMPiLTBKq75yejf_lz-xbWD_E,12287
7
7
  pdffiller/py.typed.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- pdffiller/typing.py,sha256=suxFXOGIF07R8UhrEf-ro_vNA2pXJGfe8CX64dStXTs,954
8
+ pdffiller/typing.py,sha256=dW2hGtyaNgTowUl1zsEt3ldhbHM2KfCyHAASb0RZcEs,980
9
9
  pdffiller/utils.py,sha256=pmGf3QwkhKwosk_eFCauzHM-XHp_WGVQAtZlxa7taYY,827
10
10
  pdffiller/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  pdffiller/cli/args.py,sha256=1MUtxIVgdBWJCjgp2gxUuGW-vH4qne92A7Rns2DylsQ,870
@@ -18,19 +18,20 @@ pdffiller/cli/once_argument.py,sha256=olQvNhObObS0iXVL1YCa-_lH76SV0I4FvnlgR9roqo
18
18
  pdffiller/cli/smart_formatter.py,sha256=59hwF07nKbp-P9IfbqKgMFsfbvjIw5SACCZpUF4nnuE,318
19
19
  pdffiller/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  pdffiller/cli/commands/dump_data_fields.py,sha256=YR8aENCGkCRro0jTuuvMg2HCEos9aF81aL4SxP--_2E,2262
21
- pdffiller/cli/commands/fill_form.py,sha256=R1xF8iHswn_FeYvE7oQOTDwpRf6BjqQ7y2pAHg6yRb4,4627
21
+ pdffiller/cli/commands/fill_form.py,sha256=5wjbjwYLsytY6ea-n_KDbU9UDmkN00NYj_gm25MQqJ8,4758
22
22
  pdffiller/io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  pdffiller/io/colors.py,sha256=QCBEWksTVNurOJQYO0zh1X_xxIOXxXmYSJhbCqnNjI8,1710
24
- pdffiller/io/output.py,sha256=NWV1SabnR_kXmR8tfUKQQoQ_3RV9wqiSnHqzinQDgB0,11217
24
+ pdffiller/io/custom_pdf_writer.py,sha256=rz4kPXJ0JgQhF3896OKizFdnEoPe989NruBA75HMnBQ,6960
25
+ pdffiller/io/output.py,sha256=QMASWRWmfZGG9DdtlfpWXM3VJAMgWGQwPUzoYp_9FFY,11298
25
26
  pdffiller/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
27
  pdffiller/widgets/base.py,sha256=omGVQsQgMa-ALESnUd3_94oVIYScAMl0SPhHC_DG8Lg,3613
27
28
  pdffiller/widgets/checkbox.py,sha256=iijStLAsY1G4cljW3a9NxVS_8qxJewFEw-B8jU2aKXk,1711
28
29
  pdffiller/widgets/radio.py,sha256=Db9Oc3Q8ge8qqTVPLoz3I1_SJBGyJ8KfA33ixZMr78c,1070
29
30
  pdffiller/widgets/text.py,sha256=SiuyBvZPZ6idCmtZ_05zE26iN6Rz67OfOj1fUm98YQI,2397
30
- python_pdffiller-1.1.0.dist-info/licenses/AUTHORS.rst,sha256=1_hVzMKgmoXvGgrcZC7sIbU_6PvvkB6vwqevAqzrIkQ,205
31
- python_pdffiller-1.1.0.dist-info/licenses/COPYING,sha256=ADPe-bH2wYq8nFf6EPJyovzTJyl3jSPnm09mGI8FSTo,1074
32
- python_pdffiller-1.1.0.dist-info/METADATA,sha256=yWwklFn1pQVt_lQ9EvY58kAXvQd8aeN-K4Aaju0nERU,4319
33
- python_pdffiller-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
- python_pdffiller-1.1.0.dist-info/entry_points.txt,sha256=RESKKpPPdWl0wDET96ntuFoUydALx9j0mxtbt-MEBjU,49
35
- python_pdffiller-1.1.0.dist-info/top_level.txt,sha256=5MGWCBFYlu_Ax-I5PgQkV9Xw7O48maPe9z8Qj_yVPL4,10
36
- python_pdffiller-1.1.0.dist-info/RECORD,,
31
+ python_pdffiller-1.1.1.dist-info/licenses/AUTHORS.rst,sha256=1_hVzMKgmoXvGgrcZC7sIbU_6PvvkB6vwqevAqzrIkQ,205
32
+ python_pdffiller-1.1.1.dist-info/licenses/COPYING,sha256=ADPe-bH2wYq8nFf6EPJyovzTJyl3jSPnm09mGI8FSTo,1074
33
+ python_pdffiller-1.1.1.dist-info/METADATA,sha256=G0UFHlAH4UOctSuJ6J-X0fHitovbcnnmq8LUv7zA79U,4319
34
+ python_pdffiller-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ python_pdffiller-1.1.1.dist-info/entry_points.txt,sha256=RESKKpPPdWl0wDET96ntuFoUydALx9j0mxtbt-MEBjU,49
36
+ python_pdffiller-1.1.1.dist-info/top_level.txt,sha256=5MGWCBFYlu_Ax-I5PgQkV9Xw7O48maPe9z8Qj_yVPL4,10
37
+ python_pdffiller-1.1.1.dist-info/RECORD,,