PyPDFForm 5.6.0__tar.gz → 6.0.1__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-5.6.0 → pypdfform-6.0.1}/PKG-INFO +3 -3
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/__init__.py +1 -10
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/root.py +2 -18
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/font.py +7 -4
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/hooks.py +67 -26
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/middleware/base.py +48 -1
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/middleware/checkbox.py +2 -2
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/middleware/dropdown.py +85 -14
- pypdfform-6.0.1/PyPDFForm/lib/middleware/radio.py +129 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/middleware/text.py +1 -1
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/patterns.py +16 -11
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/wrapper.py +35 -29
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm.egg-info/PKG-INFO +3 -3
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm.egg-info/SOURCES.txt +1 -1
- {pypdfform-5.6.0 → pypdfform-6.0.1}/pyproject.toml +4 -4
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_extract_middleware_attributes.py +2 -2
- pypdfform-5.6.0/tests/test_font_widths.py → pypdfform-6.0.1/tests/test_font_related.py +86 -1
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_functional.py +196 -9
- pypdfform-6.0.1/tests/test_use_full_widget_name.py +116 -0
- pypdfform-5.6.0/PyPDFForm/lib/middleware/radio.py +0 -72
- pypdfform-5.6.0/tests/test_use_full_widget_name.py +0 -55
- {pypdfform-5.6.0 → pypdfform-6.0.1}/LICENSE +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/__init__.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/common.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/create.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/entry.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/inspect.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/remove.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/schemas/__init__.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/schemas/create.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/schemas/update.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/cli/update.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/constants.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/egress.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/template.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/watermark.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/widgets/base.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/widgets/signature.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm.egg-info/requires.txt +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/README.md +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/setup.cfg +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_create_widget.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_draw_elements.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_dropdown.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_js.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_need_appearances.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_paragraph.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/tests/test_signature.py +0 -0
- {pypdfform-5.6.0 → pypdfform-6.0.1}/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
|
+
Version: 6.0.1
|
|
4
4
|
Summary: The Python library & CLI for PDF forms.
|
|
5
5
|
Author: Jinge Li
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,16 +12,16 @@ Classifier: Intended Audience :: Developers
|
|
|
12
12
|
Classifier: Operating System :: OS Independent
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
20
|
Classifier: Topic :: File Formats
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Classifier: Topic :: Text Processing
|
|
23
23
|
Classifier: Topic :: Utilities
|
|
24
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
27
|
Requires-Dist: cryptography<51.0.0,>=50.0.0
|
|
@@ -16,9 +16,7 @@ PyPDFForm aims to make PDF form automation straightforward whether it is used
|
|
|
16
16
|
from Python code or from the command line.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
__version__ = "5.6.0"
|
|
19
|
+
__version__ = "6.0.1"
|
|
22
20
|
|
|
23
21
|
from .lib.annotations import Annotations
|
|
24
22
|
from .lib.assets.blank import BlankPage
|
|
@@ -28,13 +26,6 @@ from .lib.types import PdfArray
|
|
|
28
26
|
from .lib.widgets import Fields
|
|
29
27
|
from .lib.wrapper import PdfWrapper
|
|
30
28
|
|
|
31
|
-
# TODO: figure out why `Annotation sizes differ:`
|
|
32
|
-
for logger in [
|
|
33
|
-
logging.getLogger(name) for name in getattr(logging.root.manager, "loggerDict")
|
|
34
|
-
]:
|
|
35
|
-
if "pypdf" in logger.name:
|
|
36
|
-
logger.setLevel(logging.ERROR)
|
|
37
|
-
|
|
38
29
|
__all__ = [
|
|
39
30
|
"PdfWrapper",
|
|
40
31
|
"PdfArray",
|
|
@@ -108,14 +108,6 @@ def main(
|
|
|
108
108
|
help="Generate form field appearance streams.",
|
|
109
109
|
),
|
|
110
110
|
] = False,
|
|
111
|
-
preserve_metadata: Annotated[
|
|
112
|
-
bool,
|
|
113
|
-
typer.Option(
|
|
114
|
-
"--preserve-metadata",
|
|
115
|
-
help="Preserve input PDF metadata.",
|
|
116
|
-
hidden=True,
|
|
117
|
-
),
|
|
118
|
-
] = False,
|
|
119
111
|
use_full_widget_name: Annotated[
|
|
120
112
|
bool,
|
|
121
113
|
typer.Option(
|
|
@@ -138,15 +130,12 @@ def main(
|
|
|
138
130
|
field appearances.
|
|
139
131
|
generate_appearance_streams (bool): Whether to generate form field
|
|
140
132
|
appearance streams while handling PDFs.
|
|
141
|
-
preserve_metadata (bool): Deprecated compatibility option. Input PDF
|
|
142
|
-
metadata is preserved automatically.
|
|
143
133
|
use_full_widget_name (bool): Whether widget lookups should use full
|
|
144
134
|
form field names.
|
|
145
135
|
"""
|
|
146
136
|
ctx.obj = {
|
|
147
137
|
"need_appearances": need_appearances,
|
|
148
138
|
"generate_appearance_streams": generate_appearance_streams,
|
|
149
|
-
"preserve_metadata": preserve_metadata,
|
|
150
139
|
"use_full_widget_name": use_full_widget_name,
|
|
151
140
|
}
|
|
152
141
|
|
|
@@ -163,10 +152,6 @@ def fill(
|
|
|
163
152
|
Path | None, data_file_option("YAML or JSON file with form field values.")
|
|
164
153
|
] = None,
|
|
165
154
|
output: OPTIONAL_OUTPUT_PDF = None,
|
|
166
|
-
flatten: Annotated[
|
|
167
|
-
bool,
|
|
168
|
-
typer.Option("--flatten", help="Deprecated alias for --readonly.", hidden=True),
|
|
169
|
-
] = None,
|
|
170
155
|
readonly: Annotated[
|
|
171
156
|
bool,
|
|
172
157
|
typer.Option("--readonly", help="Make form fields read-only when filling."),
|
|
@@ -191,9 +176,8 @@ def fill(
|
|
|
191
176
|
options in ``ctx.args``. Defaults to None.
|
|
192
177
|
output (Path, optional): Output PDF path. If omitted, the input PDF is
|
|
193
178
|
overwritten. Defaults to None.
|
|
194
|
-
flatten (bool, optional): Deprecated alias for `readonly`. Defaults to None.
|
|
195
179
|
readonly (bool): Whether to make form fields read-only when filling.
|
|
196
|
-
Defaults to False.
|
|
180
|
+
Defaults to False.
|
|
197
181
|
"""
|
|
198
182
|
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
199
183
|
|
|
@@ -231,4 +215,4 @@ def fill(
|
|
|
231
215
|
)
|
|
232
216
|
input_data[k] = input_data[k]["path"]
|
|
233
217
|
|
|
234
|
-
obj.fill(input_data, readonly=readonly
|
|
218
|
+
obj.fill(input_data, readonly=readonly).write(output or pdf)
|
|
@@ -10,6 +10,7 @@ for extracting font information from TTF streams and managing font names within
|
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
from functools import lru_cache
|
|
12
12
|
from io import BytesIO
|
|
13
|
+
from re import fullmatch
|
|
13
14
|
from typing import Generator
|
|
14
15
|
from uuid import uuid4
|
|
15
16
|
from zlib import compress
|
|
@@ -382,8 +383,10 @@ def _get_new_font_name(fonts: dict) -> str:
|
|
|
382
383
|
"""
|
|
383
384
|
Generates a new unique font name to avoid conflicts with existing fonts in the PDF.
|
|
384
385
|
|
|
385
|
-
This function inspects existing AcroForm resource keys that
|
|
386
|
-
|
|
386
|
+
This function inspects existing AcroForm resource keys that exactly match `/F`
|
|
387
|
+
followed by one or more ASCII decimal digits and returns the lowest unused
|
|
388
|
+
numbered key with the same prefix. Other resource names are left unchanged
|
|
389
|
+
and do not reserve a number.
|
|
387
390
|
|
|
388
391
|
Args:
|
|
389
392
|
fonts (dict): A dictionary of existing fonts in the PDF.
|
|
@@ -393,8 +396,8 @@ def _get_new_font_name(fonts: dict) -> str:
|
|
|
393
396
|
"""
|
|
394
397
|
existing = set()
|
|
395
398
|
for key in fonts:
|
|
396
|
-
if isinstance(key, str) and
|
|
397
|
-
existing.add(int(key[
|
|
399
|
+
if isinstance(key, str) and fullmatch(rf"{FONT_NAME_PREFIX}[0-9]+", key):
|
|
400
|
+
existing.add(int(key[len(FONT_NAME_PREFIX) :]))
|
|
398
401
|
|
|
399
402
|
n = 1
|
|
400
403
|
while n in existing:
|
|
@@ -314,7 +314,28 @@ def update_text_field_max_length(annot: DictionaryObject, val: int) -> None:
|
|
|
314
314
|
annot[NameObject(MaxLen)] = NumberObject(val)
|
|
315
315
|
|
|
316
316
|
|
|
317
|
-
def
|
|
317
|
+
def _normalize_geometry_value(val: int | float) -> float:
|
|
318
|
+
"""
|
|
319
|
+
Normalizes scalar geometry to a float before modifying an annotation.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
val (int | float): The coordinate or dimension to normalize.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
float: The normalized geometry value.
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
TypeError: If the value is not an integer or float, or is a boolean.
|
|
329
|
+
"""
|
|
330
|
+
# bool subclasses int, so the numeric type check alone would accept booleans.
|
|
331
|
+
if isinstance(val, bool) or not isinstance(val, (int, float)):
|
|
332
|
+
# TODO: Refactor this raise into a centralized exception raiser when one is introduced.
|
|
333
|
+
msg = "Geometry values must be integers or floats; booleans are invalid."
|
|
334
|
+
raise TypeError(msg)
|
|
335
|
+
return float(val)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def update_check_radio_size(annot: DictionaryObject, val: int | float) -> None:
|
|
318
339
|
"""
|
|
319
340
|
Updates the size of a check box or radio button annotation.
|
|
320
341
|
|
|
@@ -325,8 +346,12 @@ def update_check_radio_size(annot: DictionaryObject, val: float) -> None:
|
|
|
325
346
|
Args:
|
|
326
347
|
annot (DictionaryObject): The annotation dictionary for the check box or
|
|
327
348
|
radio button.
|
|
328
|
-
val (float): The new size (width and height) for the check box or radio button.
|
|
349
|
+
val (int | float): The new size (width and height) for the check box or radio button.
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
TypeError: If the size is not an integer or float, or is a boolean.
|
|
329
353
|
"""
|
|
354
|
+
val = _normalize_geometry_value(val)
|
|
330
355
|
rect = annot[Rect]
|
|
331
356
|
# scale from bottom left
|
|
332
357
|
new_rect = [
|
|
@@ -379,72 +404,88 @@ def update_field_readonly(annot: DictionaryObject, val: bool) -> None:
|
|
|
379
404
|
_update_field_flag(annot, READ_ONLY, val)
|
|
380
405
|
|
|
381
406
|
|
|
382
|
-
def update_field_x(annot: DictionaryObject, val: float) -> None:
|
|
407
|
+
def update_field_x(annot: DictionaryObject, val: int | float) -> None:
|
|
383
408
|
"""
|
|
384
409
|
Updates the X-coordinate (horizontal position) of a form field annotation.
|
|
385
410
|
|
|
386
411
|
This function modifies the 'Rect' entry of the annotation dictionary to change
|
|
387
412
|
the left X-coordinate of the field and adjusts the right X-coordinate by the
|
|
388
|
-
same delta to maintain the field's width.
|
|
413
|
+
same delta to maintain the field's width. Integer and float inputs are
|
|
414
|
+
normalized to floats before updating the rectangle.
|
|
389
415
|
|
|
390
416
|
Args:
|
|
391
417
|
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
392
|
-
val (float): The new X-coordinate for the left edge of the field.
|
|
418
|
+
val (int | float): The new X-coordinate for the left edge of the field.
|
|
419
|
+
|
|
420
|
+
Raises:
|
|
421
|
+
TypeError: If the coordinate is not an integer or float, or is a boolean.
|
|
393
422
|
"""
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
423
|
+
val = _normalize_geometry_value(val)
|
|
424
|
+
diff = val - annot[Rect][0]
|
|
425
|
+
annot[NameObject(Rect)][0] = FloatObject(val)
|
|
426
|
+
annot[NameObject(Rect)][2] = FloatObject(annot[NameObject(Rect)][2] + diff)
|
|
398
427
|
|
|
399
428
|
|
|
400
|
-
def update_field_y(annot: DictionaryObject, val: float) -> None:
|
|
429
|
+
def update_field_y(annot: DictionaryObject, val: int | float) -> None:
|
|
401
430
|
"""
|
|
402
431
|
Updates the Y-coordinate (vertical position) of a form field annotation.
|
|
403
432
|
|
|
404
433
|
This function modifies the 'Rect' entry of the annotation dictionary to change
|
|
405
434
|
the bottom Y-coordinate of the field and adjusts the top Y-coordinate by the
|
|
406
|
-
same delta to maintain the field's height.
|
|
435
|
+
same delta to maintain the field's height. Integer and float inputs are
|
|
436
|
+
normalized to floats before updating the rectangle.
|
|
407
437
|
|
|
408
438
|
Args:
|
|
409
439
|
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
410
|
-
val (float): The new Y-coordinate for the bottom edge of the field.
|
|
440
|
+
val (int | float): The new Y-coordinate for the bottom edge of the field.
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
TypeError: If the coordinate is not an integer or float, or is a boolean.
|
|
411
444
|
"""
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
445
|
+
val = _normalize_geometry_value(val)
|
|
446
|
+
diff = val - annot[Rect][1]
|
|
447
|
+
annot[NameObject(Rect)][1] = FloatObject(val)
|
|
448
|
+
annot[NameObject(Rect)][3] = FloatObject(annot[NameObject(Rect)][3] + diff)
|
|
416
449
|
|
|
417
450
|
|
|
418
|
-
def update_field_width(annot: DictionaryObject, val: float) -> None:
|
|
451
|
+
def update_field_width(annot: DictionaryObject, val: int | float) -> None:
|
|
419
452
|
"""
|
|
420
453
|
Updates the width of a form field annotation.
|
|
421
454
|
|
|
422
455
|
This function modifies the 'Rect' entry of the annotation dictionary to set
|
|
423
456
|
the new width of the field, adjusting the rightmost coordinate while keeping
|
|
424
|
-
the leftmost coordinate fixed.
|
|
457
|
+
the leftmost coordinate fixed. Integer and float inputs are normalized to
|
|
458
|
+
floats before updating the rectangle.
|
|
425
459
|
|
|
426
460
|
Args:
|
|
427
461
|
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
428
|
-
val (float): The new width of the field.
|
|
462
|
+
val (int | float): The new width of the field.
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
TypeError: If the width is not an integer or float, or is a boolean.
|
|
429
466
|
"""
|
|
430
|
-
|
|
431
|
-
|
|
467
|
+
val = _normalize_geometry_value(val)
|
|
468
|
+
annot[NameObject(Rect)][2] = FloatObject(annot[Rect][0] + val)
|
|
432
469
|
|
|
433
470
|
|
|
434
|
-
def update_field_height(annot: DictionaryObject, val: float) -> None:
|
|
471
|
+
def update_field_height(annot: DictionaryObject, val: int | float) -> None:
|
|
435
472
|
"""
|
|
436
473
|
Updates the height of a form field annotation.
|
|
437
474
|
|
|
438
475
|
This function modifies the 'Rect' entry of the annotation dictionary to set
|
|
439
476
|
the new height of the field, adjusting the topmost coordinate while keeping
|
|
440
|
-
the bottommost coordinate fixed.
|
|
477
|
+
the bottommost coordinate fixed. Integer and float inputs are normalized to
|
|
478
|
+
floats before updating the rectangle.
|
|
441
479
|
|
|
442
480
|
Args:
|
|
443
481
|
annot (DictionaryObject): The annotation dictionary for the form field.
|
|
444
|
-
val (float): The new height of the field.
|
|
482
|
+
val (int | float): The new height of the field.
|
|
483
|
+
|
|
484
|
+
Raises:
|
|
485
|
+
TypeError: If the height is not an integer or float, or is a boolean.
|
|
445
486
|
"""
|
|
446
|
-
|
|
447
|
-
|
|
487
|
+
val = _normalize_geometry_value(val)
|
|
488
|
+
annot[NameObject(Rect)][3] = FloatObject(annot[Rect][1] + val)
|
|
448
489
|
|
|
449
490
|
|
|
450
491
|
def update_field_tooltip(annot: DictionaryObject, val: str) -> None:
|
|
@@ -8,9 +8,29 @@ common attributes and methods for all form widgets, such as name, value,
|
|
|
8
8
|
and schema definition.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
from dataclasses import dataclass
|
|
11
12
|
from typing import Any, List, Optional, TextIO
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class PreparedValue:
|
|
17
|
+
"""
|
|
18
|
+
Represents a widget value prepared for assignment.
|
|
19
|
+
|
|
20
|
+
Preparation validates and normalizes the value without changing the widget.
|
|
21
|
+
Dropdown updates can also include replacement choices, which are stored and
|
|
22
|
+
queued for PDF updates only when the prepared value is applied.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
value (Any): The value to store in the widget.
|
|
26
|
+
choices (list | None): Replacement dropdown choices, or None when the
|
|
27
|
+
choices should remain unchanged.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
value: Any
|
|
31
|
+
choices: list | None = None
|
|
32
|
+
|
|
33
|
+
|
|
14
34
|
class Widget:
|
|
15
35
|
"""
|
|
16
36
|
Base class for form widget.
|
|
@@ -136,7 +156,34 @@ class Widget:
|
|
|
136
156
|
Args:
|
|
137
157
|
value (Any): The value to set.
|
|
138
158
|
"""
|
|
139
|
-
self.
|
|
159
|
+
self.apply_value(self.prepare_value(value))
|
|
160
|
+
|
|
161
|
+
def prepare_value(self, value: Any) -> PreparedValue:
|
|
162
|
+
"""
|
|
163
|
+
Prepares a value for assignment without changing the widget.
|
|
164
|
+
|
|
165
|
+
The base implementation retains the supplied value. Subclasses can
|
|
166
|
+
validate or normalize it and stage related attribute changes here.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
value (Any): The proposed value of the widget.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
PreparedValue: The value to apply after preparation succeeds.
|
|
173
|
+
"""
|
|
174
|
+
return PreparedValue(value)
|
|
175
|
+
|
|
176
|
+
def apply_value(self, update: PreparedValue) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Applies a previously prepared value to the widget.
|
|
179
|
+
|
|
180
|
+
This method stores the value without repeating validation. Subclasses
|
|
181
|
+
can apply related attribute changes through their existing hooks.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
update (PreparedValue): The prepared widget value to store.
|
|
185
|
+
"""
|
|
186
|
+
self._value = update.value
|
|
140
187
|
|
|
141
188
|
@property
|
|
142
189
|
def schema_definition(self) -> dict:
|
|
@@ -76,7 +76,7 @@ class Checkbox(Widget):
|
|
|
76
76
|
return {"type": "boolean", **super().schema_definition}
|
|
77
77
|
|
|
78
78
|
@property
|
|
79
|
-
def sample_value(self) -> bool | int:
|
|
79
|
+
def sample_value(self) -> bool | int | None:
|
|
80
80
|
"""
|
|
81
81
|
Returns a sample value for the checkbox.
|
|
82
82
|
|
|
@@ -84,6 +84,6 @@ class Checkbox(Widget):
|
|
|
84
84
|
checkbox field.
|
|
85
85
|
|
|
86
86
|
Returns:
|
|
87
|
-
bool | int: A sample value for the checkbox.
|
|
87
|
+
bool | int | None: A sample value for the checkbox or its subclasses.
|
|
88
88
|
"""
|
|
89
89
|
return True
|
|
@@ -6,9 +6,9 @@ This module defines the Dropdown class, which is a subclass of the
|
|
|
6
6
|
Widget class. It represents a dropdown form field in a PDF document.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from typing import Optional
|
|
9
|
+
from typing import Any, Optional
|
|
10
10
|
|
|
11
|
-
from .base import Widget
|
|
11
|
+
from .base import PreparedValue, Widget
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Dropdown(Widget):
|
|
@@ -18,7 +18,7 @@ class Dropdown(Widget):
|
|
|
18
18
|
Inherits from the Widget class and provides specific functionality
|
|
19
19
|
for handling dropdown form fields. The stored value is an option index; string
|
|
20
20
|
assignments are normalized to an existing option index or appended as a new
|
|
21
|
-
choice.
|
|
21
|
+
choice. Integer indices must identify an existing option.
|
|
22
22
|
|
|
23
23
|
Key attributes:
|
|
24
24
|
font (str): The font of the dropdown field.
|
|
@@ -81,21 +81,92 @@ class Dropdown(Widget):
|
|
|
81
81
|
|
|
82
82
|
If the value is a string, it attempts to find the corresponding
|
|
83
83
|
index in the choices list. If not found, the string value is
|
|
84
|
-
added to the choices, and its new index is used. Integer values are
|
|
85
|
-
|
|
84
|
+
added to the choices, and its new index is used. Integer values are checked
|
|
85
|
+
against the current choices before being stored.
|
|
86
86
|
|
|
87
87
|
Args:
|
|
88
88
|
value (str | int): The value to set. Can be a string
|
|
89
89
|
(option text) or an integer (index).
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If an integer index is outside the current choices.
|
|
93
|
+
"""
|
|
94
|
+
self.apply_value(self.prepare_value(value))
|
|
95
|
+
|
|
96
|
+
def prepare_value(self, value: Any) -> PreparedValue:
|
|
90
97
|
"""
|
|
98
|
+
Prepares a dropdown selection without changing its value or choices.
|
|
99
|
+
|
|
100
|
+
Integer indices are checked against the current choices. Strings are
|
|
101
|
+
normalized to an existing index or staged as a new last option. New
|
|
102
|
+
choices do not trigger hooks until the prepared selection is applied.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
value (Any): The proposed option text or index.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
PreparedValue: The selected index and any replacement choices.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If an integer index is outside the current choices.
|
|
112
|
+
"""
|
|
113
|
+
self.validate_choice_index(value)
|
|
91
114
|
if isinstance(value, str):
|
|
92
115
|
index = self._get_option_index(value)
|
|
93
116
|
if index is None:
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
choices = list(self.choices) + [value]
|
|
118
|
+
return PreparedValue(len(choices) - 1, choices)
|
|
96
119
|
value = index
|
|
97
120
|
|
|
98
|
-
|
|
121
|
+
return super().prepare_value(value)
|
|
122
|
+
|
|
123
|
+
def apply_value(self, update: PreparedValue) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Applies a previously prepared dropdown selection.
|
|
126
|
+
|
|
127
|
+
Replacement choices are stored through the existing attribute setter
|
|
128
|
+
so their PDF update hook is queued before the selected index is stored.
|
|
129
|
+
Validation and option lookup are not repeated during application.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
update (PreparedValue): The prepared selection and optional choices.
|
|
133
|
+
"""
|
|
134
|
+
if update.choices is not None:
|
|
135
|
+
self.choices = update.choices
|
|
136
|
+
super().apply_value(update)
|
|
137
|
+
|
|
138
|
+
def choice_index_schema(self) -> dict:
|
|
139
|
+
"""
|
|
140
|
+
Returns the schema definition for indices into the current choices.
|
|
141
|
+
|
|
142
|
+
Valid indices range from zero to the number of choices minus one. An
|
|
143
|
+
empty dropdown explicitly rejects every integer selection.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
dict: A dictionary representing the schema definition for indices.
|
|
147
|
+
"""
|
|
148
|
+
number_of_options = len(self.choices or [])
|
|
149
|
+
if not number_of_options:
|
|
150
|
+
return {"type": "integer", "not": {}}
|
|
151
|
+
return {"type": "integer", "minimum": 0, "maximum": number_of_options - 1}
|
|
152
|
+
|
|
153
|
+
def validate_choice_index(self, value: object) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Validates an integer selection against the current dropdown choices.
|
|
156
|
+
|
|
157
|
+
Non-integer values retain their existing handling, including strings
|
|
158
|
+
normalized during preparation and None for an unset selection.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
value (object): The proposed value of the dropdown.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
ValueError: If an integer index is outside the current choices.
|
|
165
|
+
"""
|
|
166
|
+
number_of_options = len(self.choices or [])
|
|
167
|
+
if isinstance(value, int) and not 0 <= value < number_of_options:
|
|
168
|
+
msg = f"Choice index {value} must satisfy 0 <= index < {number_of_options}."
|
|
169
|
+
raise ValueError(msg)
|
|
99
170
|
|
|
100
171
|
def _get_option_index(self, value: str) -> int | None:
|
|
101
172
|
"""
|
|
@@ -128,8 +199,7 @@ class Dropdown(Widget):
|
|
|
128
199
|
return {
|
|
129
200
|
"anyOf": [
|
|
130
201
|
{
|
|
131
|
-
|
|
132
|
-
"maximum": len(self.choices) - 1,
|
|
202
|
+
**self.choice_index_schema(),
|
|
133
203
|
**super().schema_definition,
|
|
134
204
|
},
|
|
135
205
|
{"type": "string"},
|
|
@@ -137,14 +207,15 @@ class Dropdown(Widget):
|
|
|
137
207
|
}
|
|
138
208
|
|
|
139
209
|
@property
|
|
140
|
-
def sample_value(self) -> int:
|
|
210
|
+
def sample_value(self) -> int | None:
|
|
141
211
|
"""
|
|
142
212
|
Returns a sample value for the dropdown.
|
|
143
213
|
|
|
144
214
|
The sample value is used to generate example data for the
|
|
145
|
-
dropdown field. It selects the last available option
|
|
215
|
+
dropdown field. It selects the last available option, or returns None
|
|
216
|
+
when no choices are available so sample data can omit the field.
|
|
146
217
|
|
|
147
218
|
Returns:
|
|
148
|
-
int:
|
|
219
|
+
int | None: The last option index, or None when there are no choices.
|
|
149
220
|
"""
|
|
150
|
-
return len(self.choices) - 1
|
|
221
|
+
return len(self.choices) - 1 if self.choices else None
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Module representing a radio button widget.
|
|
4
|
+
|
|
5
|
+
This module defines the Radio class, which is a subclass of the
|
|
6
|
+
Checkbox class. It represents a radio button form field in a PDF
|
|
7
|
+
document, allowing users to select one option from a group of choices.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .base import PreparedValue
|
|
13
|
+
from .checkbox import Checkbox
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Radio(Checkbox):
|
|
17
|
+
"""
|
|
18
|
+
Represents a radio button widget.
|
|
19
|
+
|
|
20
|
+
The Radio class provides a concrete implementation for radio button
|
|
21
|
+
form fields. It inherits from the Checkbox class and implements
|
|
22
|
+
the schema_definition and sample_value properties.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
name: str,
|
|
28
|
+
value: int = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initializes a radio button widget.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name (str): The name of the radio button.
|
|
35
|
+
value (int): The initial value of the radio button. Defaults to None.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
number_of_options (int): The number of options for the radio button.
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(name, value)
|
|
41
|
+
|
|
42
|
+
self.number_of_options: int = 0
|
|
43
|
+
|
|
44
|
+
def prepare_value(self, value: Any) -> PreparedValue:
|
|
45
|
+
"""
|
|
46
|
+
Prepares a radio selection without changing the current value.
|
|
47
|
+
|
|
48
|
+
Integer indices are checked against the current option count. The
|
|
49
|
+
inherited value setter and PDF filling use this same preparation step.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
value (Any): The proposed index of the option to select.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
PreparedValue: The selection to apply after validation succeeds.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If an integer index is outside the current options.
|
|
59
|
+
"""
|
|
60
|
+
self.validate_choice_index(value)
|
|
61
|
+
return super().prepare_value(value)
|
|
62
|
+
|
|
63
|
+
def choice_index_schema(self) -> dict:
|
|
64
|
+
"""
|
|
65
|
+
Returns the schema definition for indices into the current radio group.
|
|
66
|
+
|
|
67
|
+
Valid indices range from zero to the option count minus one. A group
|
|
68
|
+
with no options explicitly rejects every integer selection.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
dict: A dictionary representing the schema definition for indices.
|
|
72
|
+
"""
|
|
73
|
+
if self.number_of_options <= 0:
|
|
74
|
+
return {"type": "integer", "not": {}}
|
|
75
|
+
return {
|
|
76
|
+
"type": "integer",
|
|
77
|
+
"minimum": 0,
|
|
78
|
+
"maximum": self.number_of_options - 1,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def validate_choice_index(self, value: object) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Validates an integer selection against the current radio option count.
|
|
84
|
+
|
|
85
|
+
Non-integer values retain their existing handling, including None for
|
|
86
|
+
an unset selection. Broader value-type validation is left to callers.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
value (object): The proposed value of the radio button group.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If an integer index is outside the current options.
|
|
93
|
+
"""
|
|
94
|
+
if isinstance(value, int) and not 0 <= value < self.number_of_options:
|
|
95
|
+
msg = f"Choice index {value} must satisfy 0 <= index < {self.number_of_options}."
|
|
96
|
+
raise ValueError(msg)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def schema_definition(self) -> dict:
|
|
100
|
+
"""
|
|
101
|
+
Returns the schema definition for the radio button.
|
|
102
|
+
|
|
103
|
+
The schema definition is a dictionary that describes the
|
|
104
|
+
data type and other constraints for the radio button value,
|
|
105
|
+
which is expected to be an integer representing the index
|
|
106
|
+
of the selected option.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
dict: A dictionary representing the schema definition.
|
|
110
|
+
"""
|
|
111
|
+
return {
|
|
112
|
+
**super().schema_definition,
|
|
113
|
+
**self.choice_index_schema(),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def sample_value(self) -> int | None:
|
|
118
|
+
"""
|
|
119
|
+
Returns a sample value for the radio button.
|
|
120
|
+
|
|
121
|
+
The sample value is used to generate example data for the
|
|
122
|
+
radio button field. It returns the index of the last option
|
|
123
|
+
in the group, or None when no options are available so sample data can
|
|
124
|
+
omit the field.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
int | None: The last option index, or None when there are no options.
|
|
128
|
+
"""
|
|
129
|
+
return self.number_of_options - 1 if self.number_of_options > 0 else None
|