PyPDFForm 5.3.0__tar.gz → 5.4.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.
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PKG-INFO +1 -13
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/__init__.py +1 -1
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/common.py +124 -28
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/create.py +66 -30
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/root.py +24 -10
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/update.py +90 -24
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/PKG-INFO +1 -13
- {pypdfform-5.3.0 → pypdfform-5.4.0}/README.md +0 -12
- {pypdfform-5.3.0 → pypdfform-5.4.0}/LICENSE +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/__init__.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/entry.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/inspect.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/remove.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/schemas/__init__.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/schemas/create.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/cli/schemas/update.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/__init__.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/adapter.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/__init__.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/base.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/link.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/stamp.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/text.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/annotations/text_markup.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/assets/__init__.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/assets/bedrock.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/assets/blank.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/constants.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/coordinate.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/deprecation.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/egress.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/filler.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/font.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/hooks.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/image.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/__init__.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/base.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/checkbox.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/dropdown.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/image.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/radio.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/signature.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/middleware/text.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/patterns.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/__init__.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/circle.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/ellipse.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/image.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/line.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/rect.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/raw/text.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/template.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/types.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/utils.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/watermark.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/__init__.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/base.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/checkbox.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/dropdown.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/image.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/radio.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/signature.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/widgets/text.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm/lib/wrapper.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/SOURCES.txt +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/dependency_links.txt +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/entry_points.txt +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/requires.txt +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/PyPDFForm.egg-info/top_level.txt +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/pyproject.toml +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/setup.cfg +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_bulk_create_fields.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_create_widget.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_draw_elements.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_dropdown.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_extract_middleware_attributes.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_fill_max_length_text_field.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_font_widths.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_functional.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_generate_appearance_streams.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_js.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_need_appearances.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_paragraph.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_signature.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_use_full_widget_name.py +0 -0
- {pypdfform-5.3.0 → pypdfform-5.4.0}/tests/test_widget_attr_trigger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPDFForm
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.4.0
|
|
4
4
|
Summary: The Python library & CLI for PDF forms.
|
|
5
5
|
Author: Jinge Li
|
|
6
6
|
License-Expression: MIT
|
|
@@ -121,15 +121,3 @@ The official documentation can be found on [the GitHub page](https://chinapandam
|
|
|
121
121
|
<!-- TODO: remove WIP when finish recording -->
|
|
122
122
|
* [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
|
|
123
123
|
* [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
|
|
124
|
-
|
|
125
|
-
## Star History
|
|
126
|
-
|
|
127
|
-
This project is maintained entirely in my spare time. If you like the project please consider starring the GitHub repository. It is the best way to keep me motivated and continue making the project better.
|
|
128
|
-
|
|
129
|
-
<a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
|
|
130
|
-
<picture>
|
|
131
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
|
|
132
|
-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
133
|
-
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
134
|
-
</picture>
|
|
135
|
-
</a>
|
|
@@ -137,6 +137,41 @@ def _input_file_kind(data: Path) -> str:
|
|
|
137
137
|
return "JSON"
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
def _validate_input_data(
|
|
141
|
+
input_data: Any,
|
|
142
|
+
schema: dict,
|
|
143
|
+
source: str,
|
|
144
|
+
param_hint: str,
|
|
145
|
+
) -> Any:
|
|
146
|
+
"""
|
|
147
|
+
Validates parsed CLI input against a JSON schema.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
input_data (Any): Parsed input data to validate.
|
|
151
|
+
schema (dict): JSON schema to validate against.
|
|
152
|
+
source (str): User-facing description of the input source.
|
|
153
|
+
param_hint (str): CLI parameter associated with the input.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Any: The validated input data.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
typer.BadParameter: Raised when validation fails.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
validate(instance=input_data, schema=schema)
|
|
163
|
+
except ValidationError as exc:
|
|
164
|
+
error_path = _validation_error_path(exc)
|
|
165
|
+
location = f" at {error_path}" if error_path else ""
|
|
166
|
+
cli_bad_parameter(
|
|
167
|
+
f"Invalid {source}{location}: {exc.message}",
|
|
168
|
+
param_hint=param_hint,
|
|
169
|
+
cause=exc,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return input_data
|
|
173
|
+
|
|
174
|
+
|
|
140
175
|
def load_data_file(data: Path, schema: dict, param_hint: str) -> Any:
|
|
141
176
|
"""
|
|
142
177
|
Loads a YAML or JSON CLI input file and validates it against a schema.
|
|
@@ -164,18 +199,53 @@ def load_data_file(data: Path, schema: dict, param_hint: str) -> Any:
|
|
|
164
199
|
cause=exc,
|
|
165
200
|
)
|
|
166
201
|
|
|
202
|
+
return _validate_input_data(
|
|
203
|
+
input_data,
|
|
204
|
+
schema,
|
|
205
|
+
f"{input_kind} file",
|
|
206
|
+
param_hint,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def load_data_options(options: list[str], schema: dict, param_hint: str) -> Any:
|
|
211
|
+
"""
|
|
212
|
+
Parses command-line data options and validates them against a schema.
|
|
213
|
+
|
|
214
|
+
Options use ``--name value`` syntax. Each value is parsed with
|
|
215
|
+
:func:`yaml.safe_load`.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
options (list[str]): Unrecognized command arguments captured by Typer.
|
|
219
|
+
schema (dict): JSON schema to validate the resulting option mapping
|
|
220
|
+
against.
|
|
221
|
+
param_hint (str): CLI parameter description used in error messages.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Any: Parsed and validated option data.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
typer.BadParameter: Raised when the options cannot be parsed or the
|
|
228
|
+
resulting mapping fails validation.
|
|
229
|
+
"""
|
|
230
|
+
if any(not option.startswith("--") for option in options[::2]):
|
|
231
|
+
cli_bad_parameter(
|
|
232
|
+
"Use '--name value' pairs with valid values.",
|
|
233
|
+
param_hint=param_hint,
|
|
234
|
+
)
|
|
235
|
+
|
|
167
236
|
try:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
237
|
+
input_data = {
|
|
238
|
+
option[2:]: yaml.safe_load(value)
|
|
239
|
+
for option, value in zip(options[::2], options[1::2], strict=True)
|
|
240
|
+
}
|
|
241
|
+
except (ValueError, yaml.YAMLError) as exc:
|
|
172
242
|
cli_bad_parameter(
|
|
173
|
-
|
|
243
|
+
"Use '--name value' pairs with valid values.",
|
|
174
244
|
param_hint=param_hint,
|
|
175
245
|
cause=exc,
|
|
176
246
|
)
|
|
177
247
|
|
|
178
|
-
return input_data
|
|
248
|
+
return _validate_input_data(input_data, schema, "CLI options", param_hint)
|
|
179
249
|
|
|
180
250
|
|
|
181
251
|
def get_widget(wrapper: PdfWrapper, field: str, param_hint: str) -> Widget:
|
|
@@ -209,62 +279,88 @@ def handle_font_registration(
|
|
|
209
279
|
"""
|
|
210
280
|
Registers a custom font referenced by CLI input.
|
|
211
281
|
|
|
212
|
-
CLI input
|
|
282
|
+
CLI input may provide a file path in a `font` parameter. This helper
|
|
213
283
|
registers each unique font path on the supplied `PdfWrapper` once, assigns
|
|
214
284
|
it a generated internal font name, and mutates `params["font"]` to that
|
|
215
285
|
registered name so downstream field or element constructors can use it.
|
|
216
286
|
|
|
217
287
|
Args:
|
|
218
288
|
obj (PdfWrapper): The wrapper for the PDF currently being modified.
|
|
219
|
-
params (dict): The element or widget parameters loaded from
|
|
220
|
-
|
|
221
|
-
registered_font (dict): Mapping of source font paths to
|
|
222
|
-
`PdfWrapper` font names for the current command
|
|
289
|
+
params (dict): The element or widget parameters loaded from CLI input.
|
|
290
|
+
This dictionary is mutated when it contains a `font` key.
|
|
291
|
+
registered_font (dict): Mapping of resolved source font paths to
|
|
292
|
+
generated `PdfWrapper` font names for the current command
|
|
293
|
+
invocation.
|
|
223
294
|
"""
|
|
224
295
|
if "font" in params:
|
|
225
|
-
|
|
296
|
+
font_path = str(Path(params["font"]).resolve())
|
|
297
|
+
if font_path not in registered_font:
|
|
226
298
|
font_name = f"new_font_{len(registered_font)}"
|
|
227
|
-
obj.register_font(font_name,
|
|
228
|
-
registered_font[
|
|
229
|
-
params["font"] = registered_font[
|
|
299
|
+
obj.register_font(font_name, font_path)
|
|
300
|
+
registered_font[font_path] = font_name
|
|
301
|
+
params["font"] = registered_font[font_path]
|
|
230
302
|
|
|
231
303
|
|
|
232
304
|
def create_elements_from_file(
|
|
233
305
|
pdf: Path,
|
|
234
|
-
data: Path,
|
|
306
|
+
data: Path | None,
|
|
235
307
|
element_map: dict,
|
|
236
308
|
schema: dict,
|
|
237
309
|
method_name: str,
|
|
238
310
|
ctx: typer.Context,
|
|
239
|
-
|
|
311
|
+
file_param_hint: str,
|
|
312
|
+
options_param_hint: str,
|
|
240
313
|
output: Path | None = None,
|
|
314
|
+
element_type: str | None = None,
|
|
241
315
|
) -> None:
|
|
242
316
|
"""
|
|
243
|
-
Creates PDF elements from
|
|
317
|
+
Creates PDF elements from a data file or command-line options.
|
|
244
318
|
|
|
245
|
-
|
|
246
|
-
`
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
319
|
+
A data file groups element definitions by type, such as `text`, `image`, or
|
|
320
|
+
`highlight`. Without a data file, one element of `element_type` is created
|
|
321
|
+
from the additional command-line options stored in ``ctx.args``. The data
|
|
322
|
+
file takes precedence when both input forms are supplied. Each definition
|
|
323
|
+
is constructed after optional font registration and passed to `method_name`
|
|
324
|
+
on `PdfWrapper`.
|
|
250
325
|
|
|
251
326
|
Args:
|
|
252
327
|
pdf (Path): The path to the input PDF file.
|
|
253
|
-
data (Path): The path to the input file containing grouped
|
|
254
|
-
definitions.
|
|
328
|
+
data (Path, optional): The path to the input file containing grouped
|
|
329
|
+
element definitions. Defaults to None.
|
|
255
330
|
element_map (dict): Mapping from input group names to element classes
|
|
256
331
|
or callables used to construct each object.
|
|
257
|
-
schema (dict): JSON schema used to validate
|
|
332
|
+
schema (dict): JSON schema used to validate file or command-line input.
|
|
258
333
|
method_name (str): Name of the `PdfWrapper` method that accepts the
|
|
259
334
|
constructed elements, such as `bulk_create_fields`, `draw`, or
|
|
260
335
|
`annotate`.
|
|
261
336
|
ctx (typer.Context): Typer context containing global wrapper options in
|
|
262
337
|
`ctx.obj`.
|
|
263
|
-
|
|
338
|
+
file_param_hint (str): CLI parameter associated with the input file.
|
|
339
|
+
options_param_hint (str): CLI parameter description associated with
|
|
340
|
+
command-line options.
|
|
264
341
|
output (Path, optional): Path where the modified PDF should be saved. If
|
|
265
342
|
omitted, the input PDF is overwritten. Defaults to None.
|
|
343
|
+
element_type (str, optional): Element type selected for command-line
|
|
344
|
+
input. Defaults to None.
|
|
266
345
|
"""
|
|
267
|
-
|
|
346
|
+
if data is not None:
|
|
347
|
+
input_data = load_data_file(data, schema, file_param_hint)
|
|
348
|
+
else:
|
|
349
|
+
if element_type not in element_map:
|
|
350
|
+
choices = ", ".join(element_map)
|
|
351
|
+
cli_bad_parameter(
|
|
352
|
+
f"Choose one of: {choices}.",
|
|
353
|
+
param_hint="--type",
|
|
354
|
+
)
|
|
355
|
+
input_data = {
|
|
356
|
+
element_type: [
|
|
357
|
+
load_data_options(
|
|
358
|
+
ctx.args,
|
|
359
|
+
schema["properties"][element_type]["items"],
|
|
360
|
+
options_param_hint,
|
|
361
|
+
)
|
|
362
|
+
]
|
|
363
|
+
}
|
|
268
364
|
|
|
269
365
|
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
270
366
|
ungrouped_input = []
|
|
@@ -188,33 +188,43 @@ def merge(
|
|
|
188
188
|
|
|
189
189
|
|
|
190
190
|
@create_cli.command(
|
|
191
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
191
192
|
no_args_is_help=True,
|
|
192
|
-
help="Add form fields
|
|
193
|
+
help="Add form fields from a YAML or JSON file or command-line options.",
|
|
193
194
|
)
|
|
194
195
|
def field(
|
|
195
196
|
ctx: typer.Context,
|
|
196
197
|
pdf: INPUT_PDF,
|
|
197
198
|
data: Annotated[
|
|
198
|
-
Path, data_file_option("YAML or JSON file with form field definitions.")
|
|
199
|
-
],
|
|
199
|
+
Path | None, data_file_option("YAML or JSON file with form field definitions.")
|
|
200
|
+
] = None,
|
|
200
201
|
output: OPTIONAL_OUTPUT_PDF = None,
|
|
202
|
+
element_type: Annotated[
|
|
203
|
+
str | None,
|
|
204
|
+
typer.Option("--type", help="Type of form field to create without --file."),
|
|
205
|
+
] = None,
|
|
201
206
|
) -> None:
|
|
202
207
|
"""
|
|
203
|
-
Add form fields
|
|
208
|
+
Add form fields from a data file or command-line options.
|
|
204
209
|
|
|
205
210
|
The command maps input groups such as `text`, `check`, and `signature` to
|
|
206
|
-
PyPDFForm field classes, validates the input
|
|
207
|
-
|
|
208
|
-
`PdfWrapper.bulk_create_fields` before writing the modified PDF.
|
|
211
|
+
PyPDFForm field classes, validates the input against the CLI field schema,
|
|
212
|
+
creates the corresponding field objects, and calls
|
|
213
|
+
`PdfWrapper.bulk_create_fields` before writing the modified PDF. A data file
|
|
214
|
+
can define multiple fields grouped by type. Without `--file`, `--type` and
|
|
215
|
+
the field options define one field. The data file takes precedence when
|
|
216
|
+
both input forms are supplied.
|
|
209
217
|
|
|
210
218
|
Args:
|
|
211
219
|
ctx (typer.Context): Typer context containing global `PdfWrapper`
|
|
212
220
|
options in `ctx.obj`.
|
|
213
221
|
pdf (Path): Input PDF path.
|
|
214
|
-
data (Path): YAML or JSON file containing grouped form field
|
|
215
|
-
definitions.
|
|
222
|
+
data (Path, optional): YAML or JSON file containing grouped form field
|
|
223
|
+
definitions. Defaults to None.
|
|
216
224
|
output (Path, optional): Output PDF path. If omitted, the input PDF is
|
|
217
225
|
overwritten. Defaults to None.
|
|
226
|
+
element_type (str, optional): Form field type used when `data` is
|
|
227
|
+
omitted. Defaults to None.
|
|
218
228
|
"""
|
|
219
229
|
field_map = {
|
|
220
230
|
"text": Fields.TextField,
|
|
@@ -231,39 +241,51 @@ def field(
|
|
|
231
241
|
schema=FIELD_SCHEMA,
|
|
232
242
|
method_name="bulk_create_fields",
|
|
233
243
|
ctx=ctx,
|
|
234
|
-
|
|
244
|
+
file_param_hint="--file",
|
|
245
|
+
options_param_hint="field options",
|
|
235
246
|
output=output,
|
|
247
|
+
element_type=element_type,
|
|
236
248
|
)
|
|
237
249
|
|
|
238
250
|
|
|
239
251
|
@create_cli.command(
|
|
252
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
240
253
|
no_args_is_help=True,
|
|
241
|
-
help="Draw
|
|
254
|
+
help="Draw elements from a YAML or JSON file or command-line options.",
|
|
242
255
|
)
|
|
243
256
|
def raw(
|
|
244
257
|
ctx: typer.Context,
|
|
245
258
|
pdf: INPUT_PDF,
|
|
246
259
|
data: Annotated[
|
|
247
|
-
Path, data_file_option("YAML or JSON file with raw element definitions.")
|
|
248
|
-
],
|
|
260
|
+
Path | None, data_file_option("YAML or JSON file with raw element definitions.")
|
|
261
|
+
] = None,
|
|
249
262
|
output: OPTIONAL_OUTPUT_PDF = None,
|
|
263
|
+
element_type: Annotated[
|
|
264
|
+
str | None,
|
|
265
|
+
typer.Option("--type", help="Type of element to draw without --file."),
|
|
266
|
+
] = None,
|
|
250
267
|
) -> None:
|
|
251
268
|
"""
|
|
252
|
-
Draw
|
|
269
|
+
Draw elements from a data file or command-line options.
|
|
253
270
|
|
|
254
271
|
The command maps input groups such as `text`, `image`, and `rectangle` to
|
|
255
|
-
raw element classes, validates the input
|
|
256
|
-
|
|
257
|
-
|
|
272
|
+
raw element classes, validates the input against the CLI raw element schema,
|
|
273
|
+
creates the corresponding drawable objects, and calls `PdfWrapper.draw`
|
|
274
|
+
before writing the modified PDF. A data file can define multiple elements
|
|
275
|
+
grouped by type. Without `--file`, `--type` and the element options define
|
|
276
|
+
one element. The data file takes precedence when both input forms are
|
|
277
|
+
supplied.
|
|
258
278
|
|
|
259
279
|
Args:
|
|
260
280
|
ctx (typer.Context): Typer context containing global `PdfWrapper`
|
|
261
281
|
options in `ctx.obj`.
|
|
262
282
|
pdf (Path): Input PDF path.
|
|
263
|
-
data (Path): YAML or JSON file containing grouped raw element
|
|
264
|
-
definitions.
|
|
283
|
+
data (Path, optional): YAML or JSON file containing grouped raw element
|
|
284
|
+
definitions. Defaults to None.
|
|
265
285
|
output (Path, optional): Output PDF path. If omitted, the input PDF is
|
|
266
286
|
overwritten. Defaults to None.
|
|
287
|
+
element_type (str, optional): Element type used when `data` is omitted.
|
|
288
|
+
Defaults to None.
|
|
267
289
|
"""
|
|
268
290
|
raw_element_map = {
|
|
269
291
|
"text": RawElements.RawText,
|
|
@@ -280,39 +302,51 @@ def raw(
|
|
|
280
302
|
schema=RAW_SCHEMA,
|
|
281
303
|
method_name="draw",
|
|
282
304
|
ctx=ctx,
|
|
283
|
-
|
|
305
|
+
file_param_hint="--file",
|
|
306
|
+
options_param_hint="element options",
|
|
284
307
|
output=output,
|
|
308
|
+
element_type=element_type,
|
|
285
309
|
)
|
|
286
310
|
|
|
287
311
|
|
|
288
312
|
@create_cli.command(
|
|
313
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
289
314
|
no_args_is_help=True,
|
|
290
|
-
help="Add annotations
|
|
315
|
+
help="Add annotations from a YAML or JSON file or command-line options.",
|
|
291
316
|
)
|
|
292
317
|
def annotation(
|
|
293
318
|
ctx: typer.Context,
|
|
294
319
|
pdf: INPUT_PDF,
|
|
295
320
|
data: Annotated[
|
|
296
|
-
Path, data_file_option("YAML or JSON file with annotation definitions.")
|
|
297
|
-
],
|
|
321
|
+
Path | None, data_file_option("YAML or JSON file with annotation definitions.")
|
|
322
|
+
] = None,
|
|
298
323
|
output: OPTIONAL_OUTPUT_PDF = None,
|
|
324
|
+
element_type: Annotated[
|
|
325
|
+
str | None,
|
|
326
|
+
typer.Option("--type", help="Type of annotation to add without --file."),
|
|
327
|
+
] = None,
|
|
299
328
|
) -> None:
|
|
300
329
|
"""
|
|
301
|
-
Add annotations
|
|
330
|
+
Add annotations from a data file or command-line options.
|
|
302
331
|
|
|
303
332
|
The command maps input groups such as `text`, `link`, and `highlight` to
|
|
304
|
-
annotation classes, validates the input
|
|
305
|
-
|
|
306
|
-
`PdfWrapper.annotate` before writing the modified PDF.
|
|
333
|
+
annotation classes, validates the input against the CLI annotation schema,
|
|
334
|
+
creates the corresponding annotation objects, and calls
|
|
335
|
+
`PdfWrapper.annotate` before writing the modified PDF. A data file can
|
|
336
|
+
define multiple annotations grouped by type. Without `--file`, `--type` and
|
|
337
|
+
the annotation options define one annotation. The data file takes
|
|
338
|
+
precedence when both input forms are supplied.
|
|
307
339
|
|
|
308
340
|
Args:
|
|
309
341
|
ctx (typer.Context): Typer context containing global `PdfWrapper`
|
|
310
342
|
options in `ctx.obj`.
|
|
311
343
|
pdf (Path): Input PDF path.
|
|
312
|
-
data (Path): YAML or JSON file containing grouped annotation
|
|
313
|
-
definitions.
|
|
344
|
+
data (Path, optional): YAML or JSON file containing grouped annotation
|
|
345
|
+
definitions. Defaults to None.
|
|
314
346
|
output (Path, optional): Output PDF path. If omitted, the input PDF is
|
|
315
347
|
overwritten. Defaults to None.
|
|
348
|
+
element_type (str, optional): Annotation type used when `data` is
|
|
349
|
+
omitted. Defaults to None.
|
|
316
350
|
"""
|
|
317
351
|
annotation_map = {
|
|
318
352
|
"text": Annotations.TextAnnotation,
|
|
@@ -330,8 +364,10 @@ def annotation(
|
|
|
330
364
|
schema=ANNOTATION_SCHEMA,
|
|
331
365
|
method_name="annotate",
|
|
332
366
|
ctx=ctx,
|
|
333
|
-
|
|
367
|
+
file_param_hint="--file",
|
|
368
|
+
options_param_hint="annotation options",
|
|
334
369
|
output=output,
|
|
370
|
+
element_type=element_type,
|
|
335
371
|
)
|
|
336
372
|
|
|
337
373
|
|
|
@@ -8,7 +8,7 @@ commands. The root callback collects global flags in the Typer context so each
|
|
|
8
8
|
subcommand can initialize `PdfWrapper` with consistent settings.
|
|
9
9
|
|
|
10
10
|
Commands:
|
|
11
|
-
- `fill`: Fill an existing PDF form from
|
|
11
|
+
- `fill`: Fill an existing PDF form from a data file or field options.
|
|
12
12
|
- `create`: Create PDFs, fields, annotations, raw elements, and grid views.
|
|
13
13
|
- `inspect`: Print form metadata and field data as JSON.
|
|
14
14
|
- `update`: Modify PDF metadata, field names, properties, geometry, and scripts.
|
|
@@ -21,7 +21,13 @@ from typing import Annotated
|
|
|
21
21
|
import typer
|
|
22
22
|
|
|
23
23
|
from .. import PdfWrapper, Widgets, __version__
|
|
24
|
-
from .common import
|
|
24
|
+
from .common import (
|
|
25
|
+
INPUT_PDF,
|
|
26
|
+
OPTIONAL_OUTPUT_PDF,
|
|
27
|
+
data_file_option,
|
|
28
|
+
load_data_file,
|
|
29
|
+
load_data_options,
|
|
30
|
+
)
|
|
25
31
|
from .create import create_cli
|
|
26
32
|
from .inspect import inspect_cli
|
|
27
33
|
from .remove import remove_cli
|
|
@@ -144,15 +150,16 @@ def main(
|
|
|
144
150
|
|
|
145
151
|
|
|
146
152
|
@cli_app.command(
|
|
153
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
147
154
|
no_args_is_help=True,
|
|
148
|
-
help="Fill a PDF form
|
|
155
|
+
help="Fill a PDF form from a YAML or JSON file or command-line options.",
|
|
149
156
|
)
|
|
150
157
|
def fill(
|
|
151
158
|
ctx: typer.Context,
|
|
152
159
|
pdf: INPUT_PDF,
|
|
153
160
|
data: Annotated[
|
|
154
|
-
Path, data_file_option("YAML or JSON file with form field values.")
|
|
155
|
-
],
|
|
161
|
+
Path | None, data_file_option("YAML or JSON file with form field values.")
|
|
162
|
+
] = None,
|
|
156
163
|
output: OPTIONAL_OUTPUT_PDF = None,
|
|
157
164
|
flatten: Annotated[
|
|
158
165
|
bool,
|
|
@@ -160,19 +167,22 @@ def fill(
|
|
|
160
167
|
] = None,
|
|
161
168
|
) -> None:
|
|
162
169
|
"""
|
|
163
|
-
Fill an existing PDF form from a
|
|
170
|
+
Fill an existing PDF form from a data file or command-line options.
|
|
164
171
|
|
|
165
172
|
The command loads the input PDF with the global options stored by the root
|
|
166
173
|
callback, expands the generated schema so image and signature widgets can
|
|
167
|
-
accept path objects, validates the input
|
|
174
|
+
accept path objects, validates the input data, normalizes image and
|
|
168
175
|
signature values, and writes the filled PDF to the requested output path or
|
|
169
|
-
back to the input file.
|
|
176
|
+
back to the input file. When both a data file and field options are supplied,
|
|
177
|
+
the data file takes precedence.
|
|
170
178
|
|
|
171
179
|
Args:
|
|
172
180
|
ctx (typer.Context): Typer context containing global `PdfWrapper`
|
|
173
181
|
options in `ctx.obj`.
|
|
174
182
|
pdf (Path): Input PDF form path.
|
|
175
|
-
data (Path): YAML or JSON file containing form field values.
|
|
183
|
+
data (Path, optional): YAML or JSON file containing form field values.
|
|
184
|
+
When omitted, form field values are read from the command-line
|
|
185
|
+
options in ``ctx.args``. Defaults to None.
|
|
176
186
|
output (Path, optional): Output PDF path. If omitted, the input PDF is
|
|
177
187
|
overwritten. Defaults to None.
|
|
178
188
|
flatten (bool, optional): Whether to flatten form fields after filling.
|
|
@@ -198,7 +208,11 @@ def fill(
|
|
|
198
208
|
]
|
|
199
209
|
}
|
|
200
210
|
|
|
201
|
-
input_data =
|
|
211
|
+
input_data = (
|
|
212
|
+
load_data_file(data, schema, "--file")
|
|
213
|
+
if data is not None
|
|
214
|
+
else load_data_options(ctx.args, schema, "form field options")
|
|
215
|
+
)
|
|
202
216
|
for k, each in obj.widgets.items():
|
|
203
217
|
if (
|
|
204
218
|
k in input_data
|
|
@@ -26,6 +26,7 @@ from .common import (
|
|
|
26
26
|
get_widget,
|
|
27
27
|
handle_font_registration,
|
|
28
28
|
load_data_file,
|
|
29
|
+
load_data_options,
|
|
29
30
|
)
|
|
30
31
|
from .schemas.update import FIELD_SCHEMA, RENAME_SCHEMA
|
|
31
32
|
|
|
@@ -185,33 +186,46 @@ def bounds(
|
|
|
185
186
|
|
|
186
187
|
|
|
187
188
|
@update_cli.command(
|
|
189
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
188
190
|
no_args_is_help=True,
|
|
189
|
-
help="Rename form fields from YAML or JSON.",
|
|
191
|
+
help="Rename form fields from YAML or JSON or command-line options.",
|
|
190
192
|
)
|
|
191
193
|
def rename(
|
|
192
194
|
ctx: typer.Context,
|
|
193
195
|
pdf: INPUT_PDF,
|
|
194
196
|
data: Annotated[
|
|
195
|
-
Path, data_file_option("YAML or JSON file with form field renames.")
|
|
196
|
-
],
|
|
197
|
+
Path | None, data_file_option("YAML or JSON file with form field renames.")
|
|
198
|
+
] = None,
|
|
197
199
|
output: OPTIONAL_OUTPUT_PDF = None,
|
|
200
|
+
widget: Annotated[
|
|
201
|
+
str | None,
|
|
202
|
+
typer.Option(
|
|
203
|
+
"--field",
|
|
204
|
+
help="Form field name to rename without --file.",
|
|
205
|
+
),
|
|
206
|
+
] = None,
|
|
198
207
|
) -> None:
|
|
199
208
|
"""
|
|
200
|
-
Rename form fields
|
|
209
|
+
Rename form fields from a data file or command-line options.
|
|
201
210
|
|
|
202
|
-
The command rejects full widget name mode, validates the input
|
|
203
|
-
|
|
211
|
+
The command rejects full widget name mode, validates the input against the
|
|
212
|
+
rename schema, resolves each existing field, queues the requested key
|
|
204
213
|
updates on the `PdfWrapper`, commits the rename operations, and writes the
|
|
205
|
-
modified PDF to the requested output path or back to the input file.
|
|
214
|
+
modified PDF to the requested output path or back to the input file. A data
|
|
215
|
+
file can rename multiple fields. Without `--file`, `--field` and the rename
|
|
216
|
+
options update one field. The data file takes precedence when both input
|
|
217
|
+
forms are supplied.
|
|
206
218
|
|
|
207
219
|
Args:
|
|
208
220
|
ctx (typer.Context): Typer context containing global `PdfWrapper`
|
|
209
221
|
options in `ctx.obj`.
|
|
210
222
|
pdf (Path): Input PDF path.
|
|
211
|
-
data (Path): YAML or JSON file containing form field rename
|
|
212
|
-
definitions.
|
|
223
|
+
data (Path, optional): YAML or JSON file containing form field rename
|
|
224
|
+
definitions. Defaults to None.
|
|
213
225
|
output (Path, optional): Output PDF path. If omitted, the input PDF is
|
|
214
226
|
overwritten. Defaults to None.
|
|
227
|
+
widget (str, optional): Form field name used when `data` is omitted.
|
|
228
|
+
Defaults to None.
|
|
215
229
|
|
|
216
230
|
Raises:
|
|
217
231
|
typer.BadParameter: Raised when full widget name mode is enabled, the
|
|
@@ -224,59 +238,111 @@ def rename(
|
|
|
224
238
|
param_hint="--use-full-widget-name",
|
|
225
239
|
)
|
|
226
240
|
|
|
227
|
-
|
|
241
|
+
if data is not None:
|
|
242
|
+
input_data = load_data_file(data, RENAME_SCHEMA, "--file")
|
|
243
|
+
field_param_hint = "--file"
|
|
244
|
+
else:
|
|
245
|
+
if widget is None:
|
|
246
|
+
cli_bad_parameter(
|
|
247
|
+
"Specify a form field when --file is omitted.",
|
|
248
|
+
param_hint="--field",
|
|
249
|
+
)
|
|
250
|
+
input_data = [
|
|
251
|
+
{
|
|
252
|
+
widget: load_data_options(
|
|
253
|
+
ctx.args,
|
|
254
|
+
RENAME_SCHEMA["items"]["additionalProperties"],
|
|
255
|
+
"rename options",
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
field_param_hint = "--field"
|
|
228
260
|
|
|
229
261
|
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
230
262
|
for item in input_data:
|
|
231
263
|
for k, v in item.items():
|
|
232
|
-
|
|
233
|
-
obj.update_widget_key(
|
|
264
|
+
field_widget = get_widget(obj, k, field_param_hint)
|
|
265
|
+
obj.update_widget_key(
|
|
266
|
+
field_widget.name,
|
|
267
|
+
v["new_key"],
|
|
268
|
+
index=v.get("index", 0),
|
|
269
|
+
)
|
|
234
270
|
|
|
235
271
|
obj.commit_widget_key_updates().write(output or pdf)
|
|
236
272
|
|
|
237
273
|
|
|
238
274
|
@update_cli.command(
|
|
275
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
239
276
|
no_args_is_help=True,
|
|
240
|
-
help="Update form field properties from YAML or JSON.",
|
|
277
|
+
help="Update form field properties from YAML or JSON or command-line options.",
|
|
241
278
|
)
|
|
242
279
|
def field(
|
|
243
280
|
ctx: typer.Context,
|
|
244
281
|
pdf: INPUT_PDF,
|
|
245
282
|
data: Annotated[
|
|
246
|
-
Path
|
|
247
|
-
|
|
283
|
+
Path | None,
|
|
284
|
+
data_file_option("YAML or JSON file with form field property updates."),
|
|
285
|
+
] = None,
|
|
248
286
|
output: OPTIONAL_OUTPUT_PDF = None,
|
|
287
|
+
widget: Annotated[
|
|
288
|
+
str | None,
|
|
289
|
+
typer.Option(
|
|
290
|
+
"--field",
|
|
291
|
+
help="Form field name to update without --file.",
|
|
292
|
+
),
|
|
293
|
+
] = None,
|
|
249
294
|
) -> None:
|
|
250
295
|
"""
|
|
251
|
-
Update form field properties from a
|
|
296
|
+
Update form field properties from a data file or command-line options.
|
|
252
297
|
|
|
253
|
-
The command validates the input
|
|
254
|
-
|
|
255
|
-
|
|
298
|
+
The command validates the input against the field update schema, loads the
|
|
299
|
+
PDF with the global CLI options stored in `ctx.obj`, resolves each field
|
|
300
|
+
name, registers any referenced fonts once per invocation, assigns the
|
|
256
301
|
requested widget properties, and writes the modified PDF to the requested
|
|
257
|
-
output path or back to the input file.
|
|
302
|
+
output path or back to the input file. A data file can update multiple
|
|
303
|
+
fields. Without `--file`, `--field` and the property options update one
|
|
304
|
+
field. The data file takes precedence when both input forms are supplied.
|
|
258
305
|
|
|
259
306
|
Args:
|
|
260
307
|
ctx (typer.Context): Typer context containing global `PdfWrapper`
|
|
261
308
|
options in `ctx.obj`.
|
|
262
309
|
pdf (Path): Input PDF path.
|
|
263
|
-
data (Path): YAML or JSON file containing form field property
|
|
310
|
+
data (Path, optional): YAML or JSON file containing form field property
|
|
311
|
+
updates. Defaults to None.
|
|
264
312
|
output (Path, optional): Output PDF path. If omitted, the input PDF is
|
|
265
313
|
overwritten. Defaults to None.
|
|
314
|
+
widget (str, optional): Form field name used when `data` is omitted.
|
|
315
|
+
Defaults to None.
|
|
266
316
|
|
|
267
317
|
Raises:
|
|
268
318
|
typer.BadParameter: Raised when the input file is invalid or a requested
|
|
269
319
|
field does not exist.
|
|
270
320
|
"""
|
|
271
|
-
|
|
321
|
+
if data is not None:
|
|
322
|
+
input_data = load_data_file(data, FIELD_SCHEMA, "--file")
|
|
323
|
+
field_param_hint = "--file"
|
|
324
|
+
else:
|
|
325
|
+
if widget is None:
|
|
326
|
+
cli_bad_parameter(
|
|
327
|
+
"Specify a form field when --file is omitted.",
|
|
328
|
+
param_hint="--field",
|
|
329
|
+
)
|
|
330
|
+
input_data = {
|
|
331
|
+
widget: load_data_options(
|
|
332
|
+
ctx.args,
|
|
333
|
+
FIELD_SCHEMA["patternProperties"][".+"],
|
|
334
|
+
"field update options",
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
field_param_hint = "--field"
|
|
272
338
|
|
|
273
339
|
obj = PdfWrapper(str(pdf), **ctx.obj)
|
|
274
340
|
registered_font = {}
|
|
275
341
|
for k, each in input_data.items():
|
|
276
|
-
|
|
342
|
+
field_widget = get_widget(obj, k, field_param_hint)
|
|
277
343
|
handle_font_registration(obj, each, registered_font)
|
|
278
344
|
for param, v in each.items():
|
|
279
|
-
setattr(
|
|
345
|
+
setattr(field_widget, param, v)
|
|
280
346
|
|
|
281
347
|
obj.write(output or pdf)
|
|
282
348
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyPDFForm
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.4.0
|
|
4
4
|
Summary: The Python library & CLI for PDF forms.
|
|
5
5
|
Author: Jinge Li
|
|
6
6
|
License-Expression: MIT
|
|
@@ -121,15 +121,3 @@ The official documentation can be found on [the GitHub page](https://chinapandam
|
|
|
121
121
|
<!-- TODO: remove WIP when finish recording -->
|
|
122
122
|
* [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
|
|
123
123
|
* [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
|
|
124
|
-
|
|
125
|
-
## Star History
|
|
126
|
-
|
|
127
|
-
This project is maintained entirely in my spare time. If you like the project please consider starring the GitHub repository. It is the best way to keep me motivated and continue making the project better.
|
|
128
|
-
|
|
129
|
-
<a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
|
|
130
|
-
<picture>
|
|
131
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
|
|
132
|
-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
133
|
-
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
134
|
-
</picture>
|
|
135
|
-
</a>
|
|
@@ -72,15 +72,3 @@ The official documentation can be found on [the GitHub page](https://chinapandam
|
|
|
72
72
|
<!-- TODO: remove WIP when finish recording -->
|
|
73
73
|
* [(WIP) Video Tutorials](https://youtube.com/playlist?list=PLNz_PBu1QA-gzYg5BvyOO98q15u5Xve1L&si=8MWasKEckBzY-NRQ)
|
|
74
74
|
* [Chicago Python User Group - Dec 14, 2023](https://youtu.be/8t1RdAKwr9w?si=TLgumBNXv9H8szSn)
|
|
75
|
-
|
|
76
|
-
## Star History
|
|
77
|
-
|
|
78
|
-
This project is maintained entirely in my spare time. If you like the project please consider starring the GitHub repository. It is the best way to keep me motivated and continue making the project better.
|
|
79
|
-
|
|
80
|
-
<a href="https://www.star-history.com/?repos=chinapandaman%2FPyPDFForm&type=date&logscale=&legend=top-left">
|
|
81
|
-
<picture>
|
|
82
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&theme=dark&legend=top-left" />
|
|
83
|
-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
84
|
-
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=chinapandaman/PyPDFForm&type=date&legend=top-left" />
|
|
85
|
-
</picture>
|
|
86
|
-
</a>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|