rustpdf 0.1.0__py3-none-win_amd64.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.
- rustpdf/__init__.py +673 -0
- rustpdf/pdf_ffi.dll +0 -0
- rustpdf/py.typed +0 -0
- rustpdf-0.1.0.dist-info/METADATA +132 -0
- rustpdf-0.1.0.dist-info/RECORD +8 -0
- rustpdf-0.1.0.dist-info/WHEEL +5 -0
- rustpdf-0.1.0.dist-info/licenses/LICENSE +27 -0
- rustpdf-0.1.0.dist-info/top_level.txt +1 -0
rustpdf/__init__.py
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
"""rustpdf — Python binding over the rust-pdf C ABI.
|
|
2
|
+
|
|
3
|
+
Two layers, per ``project.md`` §1.2.1:
|
|
4
|
+
|
|
5
|
+
* a raw ``ctypes`` surface bound 1:1 against ``include/pdf.h``;
|
|
6
|
+
* idiomatic wrappers (:class:`Document`, :class:`EditableDoc`) that hide the
|
|
7
|
+
opaque handles, turn ``PdfStatus`` codes into :class:`PdfError`, and work as
|
|
8
|
+
context managers.
|
|
9
|
+
|
|
10
|
+
Nobody programs against the C ABI directly. The wrappers cover the whole product
|
|
11
|
+
surface: vector graphics, embedded fonts & text, paragraphs, images, PDF/A
|
|
12
|
+
(levels 1b–3a), tagged/accessible output, attachments, AcroForm fields,
|
|
13
|
+
manipulation (merge/split/rotate/optimize/incremental update), text extraction,
|
|
14
|
+
encryption and digital signatures.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import ctypes
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from ctypes import (
|
|
23
|
+
POINTER,
|
|
24
|
+
byref,
|
|
25
|
+
c_char_p,
|
|
26
|
+
c_double,
|
|
27
|
+
c_int,
|
|
28
|
+
c_size_t,
|
|
29
|
+
c_ubyte,
|
|
30
|
+
c_void_p,
|
|
31
|
+
)
|
|
32
|
+
from enum import IntEnum
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Document",
|
|
37
|
+
"EditableDoc",
|
|
38
|
+
"PdfError",
|
|
39
|
+
"PdfaLevel",
|
|
40
|
+
"Align",
|
|
41
|
+
"AFRelationship",
|
|
42
|
+
"Encryption",
|
|
43
|
+
"version",
|
|
44
|
+
"library_path",
|
|
45
|
+
"activate_license",
|
|
46
|
+
"extract_text",
|
|
47
|
+
"sign",
|
|
48
|
+
"timestamp",
|
|
49
|
+
"add_dss",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PdfError(RuntimeError):
|
|
54
|
+
"""Raised when a C ABI call returns a non-zero ``PdfStatus``."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---- enums (mirror the integer arguments documented in pdf.h) --------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PdfaLevel(IntEnum):
|
|
61
|
+
A1B = 0
|
|
62
|
+
A2B = 1
|
|
63
|
+
A2A = 2
|
|
64
|
+
A3B = 3
|
|
65
|
+
A3A = 4
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Align(IntEnum):
|
|
69
|
+
LEFT = 0
|
|
70
|
+
RIGHT = 1
|
|
71
|
+
CENTER = 2
|
|
72
|
+
JUSTIFY = 3
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AFRelationship(IntEnum):
|
|
76
|
+
SOURCE = 0
|
|
77
|
+
DATA = 1
|
|
78
|
+
ALTERNATIVE = 2
|
|
79
|
+
SUPPLEMENT = 3
|
|
80
|
+
UNSPECIFIED = 4
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Encryption(IntEnum):
|
|
84
|
+
RC4 = 0
|
|
85
|
+
AES128 = 1
|
|
86
|
+
AES256 = 2
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---- locate and load the shared library -----------------------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _candidate_paths() -> list[Path]:
|
|
93
|
+
if env := os.environ.get("RUSTPDF_LIB"):
|
|
94
|
+
return [Path(env)]
|
|
95
|
+
if sys.platform == "darwin":
|
|
96
|
+
name = "libpdf_ffi.dylib"
|
|
97
|
+
elif sys.platform == "win32":
|
|
98
|
+
name = "pdf_ffi.dll"
|
|
99
|
+
else:
|
|
100
|
+
name = "libpdf_ffi.so"
|
|
101
|
+
here = Path(__file__).resolve().parent
|
|
102
|
+
root = here.parents[2]
|
|
103
|
+
return [
|
|
104
|
+
here / name, # bundled inside an installed wheel
|
|
105
|
+
root / "target" / "debug" / name, # local build tree
|
|
106
|
+
root / "target" / "release" / name,
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def library_path() -> Path:
|
|
111
|
+
"""Return the path to the shared library that will be loaded."""
|
|
112
|
+
for candidate in _candidate_paths():
|
|
113
|
+
if candidate.is_file():
|
|
114
|
+
return candidate
|
|
115
|
+
raise PdfError(
|
|
116
|
+
"could not locate libpdf_ffi; build it with "
|
|
117
|
+
"`cargo build -p pdf-ffi` or set RUSTPDF_LIB"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_lib = ctypes.CDLL(str(library_path()))
|
|
122
|
+
|
|
123
|
+
_DOC = c_void_p # opaque *PdfDocument
|
|
124
|
+
_ED = c_void_p # opaque *PdfEditable
|
|
125
|
+
_U8 = POINTER(c_ubyte)
|
|
126
|
+
_OUTBUF = [POINTER(_U8), POINTER(c_size_t)]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _bind(name, restype, argtypes):
|
|
130
|
+
fn = getattr(_lib, name)
|
|
131
|
+
fn.restype = restype
|
|
132
|
+
fn.argtypes = argtypes
|
|
133
|
+
return fn
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# core
|
|
137
|
+
_version = _bind("pdf_version", c_char_p, [])
|
|
138
|
+
_last_error = _bind("pdf_last_error_message", c_char_p, [])
|
|
139
|
+
_activate_license = _bind("pdf_activate_license", c_int, [c_char_p])
|
|
140
|
+
_buffer_free = _bind("pdf_buffer_free", None, [_U8, c_size_t])
|
|
141
|
+
# document lifecycle + graphics
|
|
142
|
+
_new = _bind("pdf_document_new", _DOC, [])
|
|
143
|
+
_free = _bind("pdf_document_free", None, [_DOC])
|
|
144
|
+
_add_page = _bind("pdf_document_add_page", c_int, [_DOC])
|
|
145
|
+
_add_page_sized = _bind("pdf_document_add_page_sized", c_int, [_DOC, c_double, c_double])
|
|
146
|
+
_page_count = _bind("pdf_document_page_count", c_int, [_DOC])
|
|
147
|
+
_set_fill = _bind("pdf_page_set_fill_rgb", c_int, [_DOC, c_double, c_double, c_double])
|
|
148
|
+
_set_stroke = _bind("pdf_page_set_stroke_rgb", c_int, [_DOC, c_double, c_double, c_double])
|
|
149
|
+
_set_lw = _bind("pdf_page_set_line_width", c_int, [_DOC, c_double])
|
|
150
|
+
_rect = _bind("pdf_page_rect", c_int, [_DOC, c_double, c_double, c_double, c_double])
|
|
151
|
+
_fill = _bind("pdf_page_fill", c_int, [_DOC])
|
|
152
|
+
_stroke = _bind("pdf_page_stroke", c_int, [_DOC])
|
|
153
|
+
_save = _bind("pdf_document_save", c_int, [_DOC, c_char_p])
|
|
154
|
+
_write = _bind("pdf_document_write", c_int, [_DOC, *_OUTBUF])
|
|
155
|
+
# config
|
|
156
|
+
_pdfa = _bind("pdf_document_pdfa", c_int, [_DOC])
|
|
157
|
+
_pdfa_level = _bind("pdf_document_pdfa_level", c_int, [_DOC, c_int])
|
|
158
|
+
_tagged = _bind("pdf_document_tagged", c_int, [_DOC])
|
|
159
|
+
_set_version = _bind("pdf_document_set_version", c_int, [_DOC, c_int])
|
|
160
|
+
_set_size = _bind("pdf_document_set_default_size", c_int, [_DOC, c_double, c_double])
|
|
161
|
+
_set_info = _bind(
|
|
162
|
+
"pdf_document_set_info",
|
|
163
|
+
c_int,
|
|
164
|
+
[_DOC, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p],
|
|
165
|
+
)
|
|
166
|
+
# fonts + text
|
|
167
|
+
_add_font_file = _bind("pdf_document_add_font_file", c_int, [_DOC, c_char_p, POINTER(c_int)])
|
|
168
|
+
_add_font = _bind("pdf_document_add_font", c_int, [_DOC, _U8, c_size_t, POINTER(c_int)])
|
|
169
|
+
_show_text = _bind(
|
|
170
|
+
"pdf_page_show_text",
|
|
171
|
+
c_int,
|
|
172
|
+
[_DOC, c_int, c_double, c_double, c_double, c_char_p, c_int],
|
|
173
|
+
)
|
|
174
|
+
_paragraph = _bind(
|
|
175
|
+
"pdf_page_paragraph",
|
|
176
|
+
c_int,
|
|
177
|
+
[_DOC, c_int, c_double, c_double, c_double, c_double, c_int, c_char_p],
|
|
178
|
+
)
|
|
179
|
+
# images
|
|
180
|
+
_add_image_file = _bind("pdf_document_add_image_file", c_int, [_DOC, c_char_p, POINTER(c_int)])
|
|
181
|
+
_add_image_png = _bind("pdf_document_add_image_png", c_int, [_DOC, _U8, c_size_t, POINTER(c_int)])
|
|
182
|
+
_add_image_jpeg = _bind("pdf_document_add_image_jpeg", c_int, [_DOC, _U8, c_size_t, POINTER(c_int)])
|
|
183
|
+
_draw_image = _bind(
|
|
184
|
+
"pdf_page_draw_image", c_int, [_DOC, c_int, c_double, c_double, c_double, c_double]
|
|
185
|
+
)
|
|
186
|
+
_figure = _bind(
|
|
187
|
+
"pdf_page_figure",
|
|
188
|
+
c_int,
|
|
189
|
+
[_DOC, c_int, c_double, c_double, c_double, c_double, c_char_p],
|
|
190
|
+
)
|
|
191
|
+
# attachments + forms
|
|
192
|
+
_attach = _bind(
|
|
193
|
+
"pdf_document_attach_file",
|
|
194
|
+
c_int,
|
|
195
|
+
[_DOC, c_char_p, c_char_p, _U8, c_size_t, c_int, c_char_p],
|
|
196
|
+
)
|
|
197
|
+
_text_field = _bind(
|
|
198
|
+
"pdf_document_text_field",
|
|
199
|
+
c_int,
|
|
200
|
+
[_DOC, c_char_p, c_size_t, c_double, c_double, c_double, c_double, c_char_p, c_double],
|
|
201
|
+
)
|
|
202
|
+
_checkbox = _bind(
|
|
203
|
+
"pdf_document_checkbox",
|
|
204
|
+
c_int,
|
|
205
|
+
[_DOC, c_char_p, c_size_t, c_double, c_double, c_double, c_double, c_int],
|
|
206
|
+
)
|
|
207
|
+
_dropdown = _bind(
|
|
208
|
+
"pdf_document_dropdown",
|
|
209
|
+
c_int,
|
|
210
|
+
[_DOC, c_char_p, c_size_t, c_double, c_double, c_double, c_double, c_char_p, c_int, c_double],
|
|
211
|
+
)
|
|
212
|
+
_radio_group = _bind(
|
|
213
|
+
"pdf_document_radio_group",
|
|
214
|
+
c_int,
|
|
215
|
+
[_DOC, c_char_p, c_size_t, c_size_t, POINTER(c_double), POINTER(c_char_p), c_int],
|
|
216
|
+
)
|
|
217
|
+
# editable
|
|
218
|
+
_ed_load = _bind("pdf_editable_load", _ED, [_U8, c_size_t])
|
|
219
|
+
_ed_load_pw = _bind("pdf_editable_load_password", _ED, [_U8, c_size_t, c_char_p])
|
|
220
|
+
_ed_free = _bind("pdf_editable_free", None, [_ED])
|
|
221
|
+
_ed_page_count = _bind("pdf_editable_page_count", c_int, [_ED])
|
|
222
|
+
_ed_merge = _bind("pdf_editable_merge", c_int, [_ED, _ED])
|
|
223
|
+
_ed_rotate = _bind("pdf_editable_rotate_page", c_int, [_ED, c_size_t, c_int])
|
|
224
|
+
_ed_delete = _bind("pdf_editable_delete_page", c_int, [_ED, c_size_t])
|
|
225
|
+
_ed_reorder = _bind("pdf_editable_reorder_pages", c_int, [_ED, POINTER(c_size_t), c_size_t])
|
|
226
|
+
_ed_extract = _bind(
|
|
227
|
+
"pdf_editable_extract_pages", c_int, [_ED, POINTER(c_size_t), c_size_t, POINTER(_ED)]
|
|
228
|
+
)
|
|
229
|
+
_ed_set_info = _bind("pdf_editable_set_info", c_int, [_ED, c_char_p, c_char_p])
|
|
230
|
+
_ed_get_info = _bind("pdf_editable_get_info", c_int, [_ED, c_char_p, *_OUTBUF])
|
|
231
|
+
_ed_set_xmp = _bind("pdf_editable_set_xmp", c_int, [_ED, _U8, c_size_t])
|
|
232
|
+
_ed_overlay = _bind("pdf_editable_overlay_page", c_int, [_ED, c_size_t, _U8, c_size_t])
|
|
233
|
+
_ed_fill = _bind("pdf_editable_fill_text_field", c_int, [_ED, c_char_p, c_char_p, POINTER(c_int)])
|
|
234
|
+
_ed_optimize = _bind("pdf_editable_optimize", c_int, [_ED])
|
|
235
|
+
_ed_compact = _bind("pdf_editable_compact", c_int, [_ED, c_int])
|
|
236
|
+
_ed_encrypt = _bind("pdf_editable_encrypt", c_int, [_ED, c_int, c_char_p, c_char_p, c_int])
|
|
237
|
+
_ed_to_bytes = _bind("pdf_editable_to_bytes", c_int, [_ED, *_OUTBUF])
|
|
238
|
+
_ed_incremental = _bind("pdf_editable_to_bytes_incremental", c_int, [_ED, _U8, c_size_t, *_OUTBUF])
|
|
239
|
+
_ed_save = _bind("pdf_editable_save", c_int, [_ED, c_char_p])
|
|
240
|
+
# extract + sign
|
|
241
|
+
_extract_text = _bind("pdf_extract_text", c_int, [_U8, c_size_t, *_OUTBUF])
|
|
242
|
+
_sign = _bind(
|
|
243
|
+
"pdf_sign",
|
|
244
|
+
c_int,
|
|
245
|
+
[_U8, c_size_t, _U8, c_size_t, _U8, c_size_t, c_char_p, c_char_p, c_char_p, c_int, *_OUTBUF],
|
|
246
|
+
)
|
|
247
|
+
_timestamp = _bind(
|
|
248
|
+
"pdf_timestamp",
|
|
249
|
+
c_int,
|
|
250
|
+
[_U8, c_size_t, _U8, c_size_t, _U8, c_size_t, c_char_p, *_OUTBUF],
|
|
251
|
+
)
|
|
252
|
+
_add_dss = _bind(
|
|
253
|
+
"pdf_add_dss",
|
|
254
|
+
c_int,
|
|
255
|
+
[
|
|
256
|
+
_U8, c_size_t,
|
|
257
|
+
POINTER(_U8), POINTER(c_size_t), c_size_t,
|
|
258
|
+
POINTER(_U8), POINTER(c_size_t), c_size_t,
|
|
259
|
+
*_OUTBUF,
|
|
260
|
+
],
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---- helpers ---------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def version() -> str:
|
|
268
|
+
"""Native library version string."""
|
|
269
|
+
return _version().decode("utf-8")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def activate_license(token: str) -> None:
|
|
273
|
+
"""Activate a license token, unlocking the corporate features it grants
|
|
274
|
+
(PDF/A, signatures, encryption, accessibility). Raises :class:`PdfError`
|
|
275
|
+
if the token is forged, expired or malformed."""
|
|
276
|
+
_check(_activate_license(_enc(token)))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _check(status: int) -> None:
|
|
280
|
+
if status == 0:
|
|
281
|
+
return
|
|
282
|
+
msg = _last_error()
|
|
283
|
+
detail = msg.decode("utf-8", "replace") if msg else "unknown error"
|
|
284
|
+
raise PdfError(f"PdfStatus={status}: {detail}")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _enc(s) -> bytes | None:
|
|
288
|
+
return None if s is None else str(s).encode("utf-8")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _take(call) -> bytes:
|
|
292
|
+
"""Invoke an out-buffer producer ``call(byref(ptr), byref(len))`` and return
|
|
293
|
+
the bytes, always freeing the native buffer."""
|
|
294
|
+
ptr = _U8()
|
|
295
|
+
length = c_size_t(0)
|
|
296
|
+
_check(call(byref(ptr), byref(length)))
|
|
297
|
+
try:
|
|
298
|
+
if not ptr or length.value == 0:
|
|
299
|
+
return b""
|
|
300
|
+
return bytes(ctypes.cast(ptr, POINTER(c_ubyte * length.value)).contents)
|
|
301
|
+
finally:
|
|
302
|
+
_buffer_free(ptr, length)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _as_u8(data: bytes):
|
|
306
|
+
"""A ``(ptr, len, keepalive)`` triple for a read-only byte buffer."""
|
|
307
|
+
if not data:
|
|
308
|
+
return (None, 0, None)
|
|
309
|
+
arr = (c_ubyte * len(data)).from_buffer_copy(data)
|
|
310
|
+
return (ctypes.cast(arr, _U8), len(data), arr)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ---- Document (authoring) --------------------------------------------------
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class Document:
|
|
317
|
+
"""A PDF document being authored. Use as a context manager."""
|
|
318
|
+
|
|
319
|
+
def __init__(self) -> None:
|
|
320
|
+
h = _new()
|
|
321
|
+
if not h:
|
|
322
|
+
raise PdfError("pdf_document_new returned NULL")
|
|
323
|
+
self._h = h
|
|
324
|
+
|
|
325
|
+
def __enter__(self) -> "Document":
|
|
326
|
+
return self
|
|
327
|
+
|
|
328
|
+
def __exit__(self, *exc) -> None:
|
|
329
|
+
self.close()
|
|
330
|
+
|
|
331
|
+
def close(self) -> None:
|
|
332
|
+
if getattr(self, "_h", None):
|
|
333
|
+
_free(self._h)
|
|
334
|
+
self._h = None
|
|
335
|
+
|
|
336
|
+
def _ptr(self):
|
|
337
|
+
if not self._h:
|
|
338
|
+
raise PdfError("operation on a closed Document")
|
|
339
|
+
return self._h
|
|
340
|
+
|
|
341
|
+
# configuration
|
|
342
|
+
def pdfa(self, level: PdfaLevel | None = None) -> "Document":
|
|
343
|
+
if level is None:
|
|
344
|
+
_check(_pdfa(self._ptr()))
|
|
345
|
+
else:
|
|
346
|
+
_check(_pdfa_level(self._ptr(), int(level)))
|
|
347
|
+
return self
|
|
348
|
+
|
|
349
|
+
def tagged(self) -> "Document":
|
|
350
|
+
_check(_tagged(self._ptr()))
|
|
351
|
+
return self
|
|
352
|
+
|
|
353
|
+
def set_version(self, v: int) -> "Document":
|
|
354
|
+
_check(_set_version(self._ptr(), int(v)))
|
|
355
|
+
return self
|
|
356
|
+
|
|
357
|
+
def set_default_size(self, width: float, height: float) -> "Document":
|
|
358
|
+
_check(_set_size(self._ptr(), width, height))
|
|
359
|
+
return self
|
|
360
|
+
|
|
361
|
+
def set_info(
|
|
362
|
+
self,
|
|
363
|
+
title=None,
|
|
364
|
+
author=None,
|
|
365
|
+
subject=None,
|
|
366
|
+
keywords=None,
|
|
367
|
+
creator=None,
|
|
368
|
+
) -> "Document":
|
|
369
|
+
_check(
|
|
370
|
+
_set_info(
|
|
371
|
+
self._ptr(),
|
|
372
|
+
_enc(title),
|
|
373
|
+
_enc(author),
|
|
374
|
+
_enc(subject),
|
|
375
|
+
_enc(keywords),
|
|
376
|
+
_enc(creator),
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
return self
|
|
380
|
+
|
|
381
|
+
# pages + graphics
|
|
382
|
+
def add_page(self, size: tuple[float, float] | None = None) -> "Document":
|
|
383
|
+
if size is None:
|
|
384
|
+
_check(_add_page(self._ptr()))
|
|
385
|
+
else:
|
|
386
|
+
_check(_add_page_sized(self._ptr(), size[0], size[1]))
|
|
387
|
+
return self
|
|
388
|
+
|
|
389
|
+
def set_fill_rgb(self, r, g, b) -> "Document":
|
|
390
|
+
_check(_set_fill(self._ptr(), r, g, b))
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
def set_stroke_rgb(self, r, g, b) -> "Document":
|
|
394
|
+
_check(_set_stroke(self._ptr(), r, g, b))
|
|
395
|
+
return self
|
|
396
|
+
|
|
397
|
+
def set_line_width(self, w) -> "Document":
|
|
398
|
+
_check(_set_lw(self._ptr(), w))
|
|
399
|
+
return self
|
|
400
|
+
|
|
401
|
+
def rect(self, x, y, w, h) -> "Document":
|
|
402
|
+
_check(_rect(self._ptr(), x, y, w, h))
|
|
403
|
+
return self
|
|
404
|
+
|
|
405
|
+
def fill(self) -> "Document":
|
|
406
|
+
_check(_fill(self._ptr()))
|
|
407
|
+
return self
|
|
408
|
+
|
|
409
|
+
def stroke(self) -> "Document":
|
|
410
|
+
_check(_stroke(self._ptr()))
|
|
411
|
+
return self
|
|
412
|
+
|
|
413
|
+
# fonts + text
|
|
414
|
+
def add_font_file(self, path) -> int:
|
|
415
|
+
fid = c_int(-1)
|
|
416
|
+
_check(_add_font_file(self._ptr(), _enc(path), byref(fid)))
|
|
417
|
+
return fid.value
|
|
418
|
+
|
|
419
|
+
def add_font(self, data: bytes) -> int:
|
|
420
|
+
ptr, n, _keep = _as_u8(bytes(data))
|
|
421
|
+
fid = c_int(-1)
|
|
422
|
+
_check(_add_font(self._ptr(), ptr, n, byref(fid)))
|
|
423
|
+
return fid.value
|
|
424
|
+
|
|
425
|
+
def show_text(self, font: int, size: float, x: float, y: float, text: str,
|
|
426
|
+
heading_level: int = 0) -> "Document":
|
|
427
|
+
_check(_show_text(self._ptr(), font, size, x, y, _enc(text), heading_level))
|
|
428
|
+
return self
|
|
429
|
+
|
|
430
|
+
def paragraph(self, font: int, size: float, x: float, y: float, width: float,
|
|
431
|
+
text: str, align: Align = Align.LEFT) -> "Document":
|
|
432
|
+
_check(_paragraph(self._ptr(), font, size, x, y, width, int(align), _enc(text)))
|
|
433
|
+
return self
|
|
434
|
+
|
|
435
|
+
# images
|
|
436
|
+
def add_image_file(self, path) -> int:
|
|
437
|
+
iid = c_int(-1)
|
|
438
|
+
_check(_add_image_file(self._ptr(), _enc(path), byref(iid)))
|
|
439
|
+
return iid.value
|
|
440
|
+
|
|
441
|
+
def add_image_png(self, data: bytes) -> int:
|
|
442
|
+
ptr, n, _keep = _as_u8(bytes(data))
|
|
443
|
+
iid = c_int(-1)
|
|
444
|
+
_check(_add_image_png(self._ptr(), ptr, n, byref(iid)))
|
|
445
|
+
return iid.value
|
|
446
|
+
|
|
447
|
+
def add_image_jpeg(self, data: bytes) -> int:
|
|
448
|
+
ptr, n, _keep = _as_u8(bytes(data))
|
|
449
|
+
iid = c_int(-1)
|
|
450
|
+
_check(_add_image_jpeg(self._ptr(), ptr, n, byref(iid)))
|
|
451
|
+
return iid.value
|
|
452
|
+
|
|
453
|
+
def draw_image(self, image: int, x, y, w, h) -> "Document":
|
|
454
|
+
_check(_draw_image(self._ptr(), image, x, y, w, h))
|
|
455
|
+
return self
|
|
456
|
+
|
|
457
|
+
def figure(self, image: int, x, y, w, h, alt: str) -> "Document":
|
|
458
|
+
_check(_figure(self._ptr(), image, x, y, w, h, _enc(alt)))
|
|
459
|
+
return self
|
|
460
|
+
|
|
461
|
+
# attachments
|
|
462
|
+
def attach_file(self, name: str, mime: str, data: bytes,
|
|
463
|
+
relationship: AFRelationship = AFRelationship.SOURCE,
|
|
464
|
+
description: str = "") -> "Document":
|
|
465
|
+
ptr, n, _keep = _as_u8(bytes(data))
|
|
466
|
+
_check(_attach(self._ptr(), _enc(name), _enc(mime), ptr, n,
|
|
467
|
+
int(relationship), _enc(description)))
|
|
468
|
+
return self
|
|
469
|
+
|
|
470
|
+
# forms
|
|
471
|
+
def text_field(self, name, page, rect, value="", size=0.0) -> "Document":
|
|
472
|
+
x0, y0, x1, y1 = rect
|
|
473
|
+
_check(_text_field(self._ptr(), _enc(name), page, x0, y0, x1, y1, _enc(value), size))
|
|
474
|
+
return self
|
|
475
|
+
|
|
476
|
+
def checkbox(self, name, page, rect, checked=False) -> "Document":
|
|
477
|
+
x0, y0, x1, y1 = rect
|
|
478
|
+
_check(_checkbox(self._ptr(), _enc(name), page, x0, y0, x1, y1, 1 if checked else 0))
|
|
479
|
+
return self
|
|
480
|
+
|
|
481
|
+
def dropdown(self, name, page, rect, options, selected=None, size=0.0) -> "Document":
|
|
482
|
+
x0, y0, x1, y1 = rect
|
|
483
|
+
joined = "\n".join(options)
|
|
484
|
+
sel = -1 if selected is None else int(selected)
|
|
485
|
+
_check(_dropdown(self._ptr(), _enc(name), page, x0, y0, x1, y1, _enc(joined), sel, size))
|
|
486
|
+
return self
|
|
487
|
+
|
|
488
|
+
def radio_group(self, name, page, buttons, selected=None) -> "Document":
|
|
489
|
+
count = len(buttons)
|
|
490
|
+
rects = (c_double * (count * 4))()
|
|
491
|
+
exports = (c_char_p * count)()
|
|
492
|
+
keep = []
|
|
493
|
+
for i, (rect, export) in enumerate(buttons):
|
|
494
|
+
rects[i * 4], rects[i * 4 + 1], rects[i * 4 + 2], rects[i * 4 + 3] = rect
|
|
495
|
+
b = _enc(export)
|
|
496
|
+
keep.append(b)
|
|
497
|
+
exports[i] = b
|
|
498
|
+
sel = -1 if selected is None else int(selected)
|
|
499
|
+
_check(_radio_group(self._ptr(), _enc(name), page, count, rects, exports, sel))
|
|
500
|
+
return self
|
|
501
|
+
|
|
502
|
+
# output
|
|
503
|
+
@property
|
|
504
|
+
def page_count(self) -> int:
|
|
505
|
+
return _page_count(self._ptr())
|
|
506
|
+
|
|
507
|
+
def to_bytes(self) -> bytes:
|
|
508
|
+
return _take(lambda p, n: _write(self._ptr(), p, n))
|
|
509
|
+
|
|
510
|
+
def save(self, path) -> None:
|
|
511
|
+
_check(_save(self._ptr(), _enc(path)))
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# ---- EditableDoc (manipulation) -------------------------------------------
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class EditableDoc:
|
|
518
|
+
"""An existing PDF loaded for manipulation. Use as a context manager."""
|
|
519
|
+
|
|
520
|
+
def __init__(self, handle) -> None:
|
|
521
|
+
if not handle:
|
|
522
|
+
raise PdfError(_last_error_text())
|
|
523
|
+
self._h = handle
|
|
524
|
+
|
|
525
|
+
@classmethod
|
|
526
|
+
def load(cls, data: bytes, password: str | None = None) -> "EditableDoc":
|
|
527
|
+
ptr, n, _keep = _as_u8(bytes(data))
|
|
528
|
+
if password is None:
|
|
529
|
+
return cls(_ed_load(ptr, n))
|
|
530
|
+
return cls(_ed_load_pw(ptr, n, _enc(password)))
|
|
531
|
+
|
|
532
|
+
@classmethod
|
|
533
|
+
def load_file(cls, path, password: str | None = None) -> "EditableDoc":
|
|
534
|
+
return cls.load(Path(path).read_bytes(), password)
|
|
535
|
+
|
|
536
|
+
def __enter__(self) -> "EditableDoc":
|
|
537
|
+
return self
|
|
538
|
+
|
|
539
|
+
def __exit__(self, *exc) -> None:
|
|
540
|
+
self.close()
|
|
541
|
+
|
|
542
|
+
def close(self) -> None:
|
|
543
|
+
if getattr(self, "_h", None):
|
|
544
|
+
_ed_free(self._h)
|
|
545
|
+
self._h = None
|
|
546
|
+
|
|
547
|
+
def _ptr(self):
|
|
548
|
+
if not self._h:
|
|
549
|
+
raise PdfError("operation on a closed EditableDoc")
|
|
550
|
+
return self._h
|
|
551
|
+
|
|
552
|
+
@property
|
|
553
|
+
def page_count(self) -> int:
|
|
554
|
+
return _ed_page_count(self._ptr())
|
|
555
|
+
|
|
556
|
+
def merge(self, other: "EditableDoc") -> "EditableDoc":
|
|
557
|
+
_check(_ed_merge(self._ptr(), other._ptr()))
|
|
558
|
+
return self
|
|
559
|
+
|
|
560
|
+
def rotate_page(self, index: int, degrees: int) -> "EditableDoc":
|
|
561
|
+
_check(_ed_rotate(self._ptr(), index, degrees))
|
|
562
|
+
return self
|
|
563
|
+
|
|
564
|
+
def delete_page(self, index: int) -> "EditableDoc":
|
|
565
|
+
_check(_ed_delete(self._ptr(), index))
|
|
566
|
+
return self
|
|
567
|
+
|
|
568
|
+
def reorder_pages(self, order: list[int]) -> "EditableDoc":
|
|
569
|
+
arr = (c_size_t * len(order))(*order)
|
|
570
|
+
_check(_ed_reorder(self._ptr(), arr, len(order)))
|
|
571
|
+
return self
|
|
572
|
+
|
|
573
|
+
def extract_pages(self, indices: list[int]) -> "EditableDoc":
|
|
574
|
+
arr = (c_size_t * len(indices))(*indices)
|
|
575
|
+
out = _ED()
|
|
576
|
+
_check(_ed_extract(self._ptr(), arr, len(indices), byref(out)))
|
|
577
|
+
return EditableDoc(out)
|
|
578
|
+
|
|
579
|
+
def set_info(self, key: str, value: str) -> "EditableDoc":
|
|
580
|
+
_check(_ed_set_info(self._ptr(), _enc(key), _enc(value)))
|
|
581
|
+
return self
|
|
582
|
+
|
|
583
|
+
def get_info(self, key: str) -> str:
|
|
584
|
+
return _take(lambda p, n: _ed_get_info(self._ptr(), _enc(key), p, n)).decode("utf-8")
|
|
585
|
+
|
|
586
|
+
def set_xmp(self, xml: bytes) -> "EditableDoc":
|
|
587
|
+
ptr, n, _keep = _as_u8(bytes(xml))
|
|
588
|
+
_check(_ed_set_xmp(self._ptr(), ptr, n))
|
|
589
|
+
return self
|
|
590
|
+
|
|
591
|
+
def overlay_page(self, index: int, content: bytes) -> "EditableDoc":
|
|
592
|
+
ptr, n, _keep = _as_u8(bytes(content))
|
|
593
|
+
_check(_ed_overlay(self._ptr(), index, ptr, n))
|
|
594
|
+
return self
|
|
595
|
+
|
|
596
|
+
def fill_text_field(self, name: str, value: str) -> bool:
|
|
597
|
+
found = c_int(0)
|
|
598
|
+
_check(_ed_fill(self._ptr(), _enc(name), _enc(value), byref(found)))
|
|
599
|
+
return bool(found.value)
|
|
600
|
+
|
|
601
|
+
def optimize(self) -> "EditableDoc":
|
|
602
|
+
_check(_ed_optimize(self._ptr()))
|
|
603
|
+
return self
|
|
604
|
+
|
|
605
|
+
def compact(self, on: bool = True) -> "EditableDoc":
|
|
606
|
+
_check(_ed_compact(self._ptr(), 1 if on else 0))
|
|
607
|
+
return self
|
|
608
|
+
|
|
609
|
+
def encrypt(self, user: str = "", owner: str = "",
|
|
610
|
+
method: Encryption = Encryption.AES256, read_only: bool = False) -> "EditableDoc":
|
|
611
|
+
_check(_ed_encrypt(self._ptr(), int(method), _enc(user), _enc(owner), 1 if read_only else 0))
|
|
612
|
+
return self
|
|
613
|
+
|
|
614
|
+
def to_bytes(self) -> bytes:
|
|
615
|
+
return _take(lambda p, n: _ed_to_bytes(self._ptr(), p, n))
|
|
616
|
+
|
|
617
|
+
def to_bytes_incremental(self, original: bytes) -> bytes:
|
|
618
|
+
ptr, n, _keep = _as_u8(bytes(original))
|
|
619
|
+
return _take(lambda p, ln: _ed_incremental(self._ptr(), ptr, n, p, ln))
|
|
620
|
+
|
|
621
|
+
def save(self, path) -> None:
|
|
622
|
+
_check(_ed_save(self._ptr(), _enc(path)))
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _last_error_text() -> str:
|
|
626
|
+
msg = _last_error()
|
|
627
|
+
return msg.decode("utf-8", "replace") if msg else "operation failed"
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
# ---- module-level functions ------------------------------------------------
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def extract_text(data: bytes) -> str:
|
|
634
|
+
"""Extract a document's text (Unicode via ``ToUnicode``)."""
|
|
635
|
+
ptr, n, _keep = _as_u8(bytes(data))
|
|
636
|
+
return _take(lambda p, ln: _extract_text(ptr, n, p, ln)).decode("utf-8")
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def sign(pdf: bytes, key_der: bytes, cert_der: bytes, *, reason=None,
|
|
640
|
+
location=None, name=None, pades=False) -> bytes:
|
|
641
|
+
"""Sign ``pdf`` (PKCS#7 detached, incremental update). ``pades=True`` →
|
|
642
|
+
PAdES-B-B."""
|
|
643
|
+
pp, pn, _k1 = _as_u8(bytes(pdf))
|
|
644
|
+
kp, kn, _k2 = _as_u8(bytes(key_der))
|
|
645
|
+
cp, cn, _k3 = _as_u8(bytes(cert_der))
|
|
646
|
+
return _take(
|
|
647
|
+
lambda p, ln: _sign(pp, pn, kp, kn, cp, cn, _enc(reason), _enc(location),
|
|
648
|
+
_enc(name), 1 if pades else 0, p, ln)
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def timestamp(pdf: bytes, tsa_key_der: bytes, tsa_cert_der: bytes, *, date=None) -> bytes:
|
|
653
|
+
"""Append a document timestamp (``/DocTimeStamp``, PAdES-B-LTA)."""
|
|
654
|
+
pp, pn, _k1 = _as_u8(bytes(pdf))
|
|
655
|
+
kp, kn, _k2 = _as_u8(bytes(tsa_key_der))
|
|
656
|
+
cp, cn, _k3 = _as_u8(bytes(tsa_cert_der))
|
|
657
|
+
return _take(lambda p, ln: _timestamp(pp, pn, kp, kn, cp, cn, _enc(date), p, ln))
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def add_dss(pdf: bytes, certs=(), crls=()) -> bytes:
|
|
661
|
+
"""Append a Document Security Store (``/DSS``, PAdES-B-LT)."""
|
|
662
|
+
pp, pn, _k0 = _as_u8(bytes(pdf))
|
|
663
|
+
|
|
664
|
+
def arrays(items):
|
|
665
|
+
items = [bytes(x) for x in items]
|
|
666
|
+
keep = [(c_ubyte * len(x)).from_buffer_copy(x) for x in items]
|
|
667
|
+
ptrs = (_U8 * len(items))(*[ctypes.cast(k, _U8) for k in keep])
|
|
668
|
+
lens = (c_size_t * len(items))(*[len(x) for x in items])
|
|
669
|
+
return ptrs, lens, len(items), keep
|
|
670
|
+
|
|
671
|
+
cp, cl, cc, _kc = arrays(certs)
|
|
672
|
+
rp, rl, rc, _kr = arrays(crls)
|
|
673
|
+
return _take(lambda p, ln: _add_dss(pp, pn, cp, cl, cc, rp, rl, rc, p, ln))
|
rustpdf/pdf_ffi.dll
ADDED
|
Binary file
|
rustpdf/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rustpdf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate, manipulate, sign and validate PDFs — Python binding for the rust-pdf core
|
|
5
|
+
Author-email: rust-pdf <edivanteixeira@gmail.com>
|
|
6
|
+
License: rust-pdf — Proprietary Software License
|
|
7
|
+
=======================================
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 rust-pdf. All rights reserved.
|
|
10
|
+
|
|
11
|
+
This software and its source code (the "Software") are proprietary and
|
|
12
|
+
confidential. Basic PDF generation is available free of charge; corporate
|
|
13
|
+
features (PDF/A, accessibility/tagged output, encryption and digital
|
|
14
|
+
signatures) require a valid, paid license token activated at runtime.
|
|
15
|
+
|
|
16
|
+
Subject to the terms of a separate commercial agreement, you are granted a
|
|
17
|
+
non-exclusive, non-transferable right to use the Software. You may NOT, without
|
|
18
|
+
prior written permission:
|
|
19
|
+
|
|
20
|
+
* redistribute, sublicense, sell or lease the Software;
|
|
21
|
+
* reverse engineer, decompile or disassemble the native components, except to
|
|
22
|
+
the extent such restriction is prohibited by applicable law;
|
|
23
|
+
* remove or alter any proprietary notices.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
27
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
28
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
29
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
30
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
31
|
+
|
|
32
|
+
For licensing inquiries, contact: edivanteixeira@gmail.com
|
|
33
|
+
|
|
34
|
+
Project-URL: Homepage, https://rustpdf.dev
|
|
35
|
+
Project-URL: Documentation, https://rustpdf.dev/docs/python
|
|
36
|
+
Project-URL: Repository, https://github.com/rustpdf/rustpdf
|
|
37
|
+
Project-URL: Issues, https://github.com/rustpdf/rustpdf/issues
|
|
38
|
+
Keywords: pdf,pdf/a,pdf/ua,accessibility,signature,acroform
|
|
39
|
+
Classifier: Programming Language :: Python :: 3
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
45
|
+
Classifier: Programming Language :: Rust
|
|
46
|
+
Classifier: License :: Other/Proprietary License
|
|
47
|
+
Classifier: Operating System :: MacOS
|
|
48
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
49
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
50
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
51
|
+
Classifier: Topic :: Office/Business
|
|
52
|
+
Requires-Python: >=3.9
|
|
53
|
+
Description-Content-Type: text/markdown
|
|
54
|
+
License-File: LICENSE
|
|
55
|
+
Dynamic: license-file
|
|
56
|
+
|
|
57
|
+
# rustpdf (Python binding)
|
|
58
|
+
|
|
59
|
+
Idiomatic Python over the `rust-pdf` C ABI (`libpdf_ffi`). It mirrors the full
|
|
60
|
+
product surface: vector graphics, embedded/subsetted fonts and text, wrapping
|
|
61
|
+
paragraphs, images, **PDF/A** (levels 1b–3a), **tagged/accessible** output,
|
|
62
|
+
embedded file attachments, **AcroForm** fields, manipulation
|
|
63
|
+
(merge/split/rotate/optimize/incremental update), **text extraction**,
|
|
64
|
+
**encryption** (RC4 / AES-128 / AES-256) and **digital signatures** (PKCS#7 /
|
|
65
|
+
PAdES).
|
|
66
|
+
|
|
67
|
+
Two layers, per the project's porting strategy:
|
|
68
|
+
|
|
69
|
+
* a raw `ctypes` surface bound 1:1 against `include/pdf.h`;
|
|
70
|
+
* `Document` / `EditableDoc` wrappers that hide opaque handles, raise
|
|
71
|
+
`PdfError` on non-zero status codes, and act as context managers.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
pip install rustpdf
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Platform wheels (macOS arm64, manylinux_2_28 x86_64/aarch64, Windows x64)
|
|
80
|
+
bundle the native `libpdf_ffi` library — no Rust toolchain needed to install.
|
|
81
|
+
Basic PDF generation is free; corporate features (PDF/A, accessibility,
|
|
82
|
+
encryption, signatures) unlock with a license token via the `RUSTPDF_LICENSE`
|
|
83
|
+
env var. See <https://rustpdf.dev>.
|
|
84
|
+
|
|
85
|
+
## Loading the native library
|
|
86
|
+
|
|
87
|
+
The wrapper finds `libpdf_ffi` in this order:
|
|
88
|
+
|
|
89
|
+
1. `$RUSTPDF_LIB` (explicit path);
|
|
90
|
+
2. bundled next to `rustpdf/__init__.py` (installed wheel);
|
|
91
|
+
3. the build tree (`target/debug` then `target/release`).
|
|
92
|
+
|
|
93
|
+
Build it from the repo root with `cargo build -p pdf-ffi`.
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import rustpdf
|
|
99
|
+
|
|
100
|
+
# Author an accessible PDF/A-2a document.
|
|
101
|
+
with rustpdf.Document() as doc:
|
|
102
|
+
doc.pdfa(rustpdf.PdfaLevel.A2A).set_info(title="Report", author="me")
|
|
103
|
+
f = doc.add_font_file("assets/fonts/Roboto-Regular.ttf")
|
|
104
|
+
doc.add_page()
|
|
105
|
+
doc.show_text(f, 20, 72, 760, "Title", heading_level=1)
|
|
106
|
+
doc.paragraph(f, 12, 72, 720, 450, "A wrapping, justified body…",
|
|
107
|
+
rustpdf.Align.JUSTIFY)
|
|
108
|
+
data = doc.to_bytes()
|
|
109
|
+
|
|
110
|
+
print(rustpdf.extract_text(data))
|
|
111
|
+
|
|
112
|
+
# Manipulate an existing file (non-destructive incremental update).
|
|
113
|
+
with rustpdf.EditableDoc.load(data) as ed:
|
|
114
|
+
ed.set_info("Subject", "Edited")
|
|
115
|
+
updated = ed.to_bytes_incremental(data)
|
|
116
|
+
|
|
117
|
+
# Encrypt (AES-256).
|
|
118
|
+
with rustpdf.EditableDoc.load(data) as ed:
|
|
119
|
+
ed.encrypt(owner="owner", method=rustpdf.Encryption.AES256)
|
|
120
|
+
ed.save("secured.pdf")
|
|
121
|
+
|
|
122
|
+
# Sign (PKCS#7 detached / PAdES).
|
|
123
|
+
signed = rustpdf.sign(data, key_der, cert_der, reason="Approved", pades=True)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Testing
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
cargo build -p pdf-ffi
|
|
130
|
+
python3 bindings/python/test_binding.py # dogfood + full-surface exercise
|
|
131
|
+
# or: make python-test
|
|
132
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
rustpdf/__init__.py,sha256=6xx0t83vBuvSg1UIMg07NnOK_R04tpL7OOOTF2ApNu4,22849
|
|
2
|
+
rustpdf/pdf_ffi.dll,sha256=MRhEaYM8qUzrPJTQmQN5wxKBl0sC_xSg7pwLU3ZaRKU,3526144
|
|
3
|
+
rustpdf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
rustpdf-0.1.0.dist-info/licenses/LICENSE,sha256=_SdLKMpHrGhuAPhqxM-1zB1ld8qVuGuI3c40-eNZVLg,1398
|
|
5
|
+
rustpdf-0.1.0.dist-info/METADATA,sha256=e4ZW_wnoZgVqyXrfzg9eXQAh9McbDK-QJ_m9t_15CVA,5452
|
|
6
|
+
rustpdf-0.1.0.dist-info/WHEEL,sha256=GjDPPQwEcripVP6P2r3RxLa-h5Lb9ifGB7FYYtbLDT0,98
|
|
7
|
+
rustpdf-0.1.0.dist-info/top_level.txt,sha256=EoBI7aEBa_idfnITOr7pMPbrbTsbcWHVsN2t0zTScDE,8
|
|
8
|
+
rustpdf-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
rust-pdf — Proprietary Software License
|
|
2
|
+
=======================================
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 rust-pdf. All rights reserved.
|
|
5
|
+
|
|
6
|
+
This software and its source code (the "Software") are proprietary and
|
|
7
|
+
confidential. Basic PDF generation is available free of charge; corporate
|
|
8
|
+
features (PDF/A, accessibility/tagged output, encryption and digital
|
|
9
|
+
signatures) require a valid, paid license token activated at runtime.
|
|
10
|
+
|
|
11
|
+
Subject to the terms of a separate commercial agreement, you are granted a
|
|
12
|
+
non-exclusive, non-transferable right to use the Software. You may NOT, without
|
|
13
|
+
prior written permission:
|
|
14
|
+
|
|
15
|
+
* redistribute, sublicense, sell or lease the Software;
|
|
16
|
+
* reverse engineer, decompile or disassemble the native components, except to
|
|
17
|
+
the extent such restriction is prohibited by applicable law;
|
|
18
|
+
* remove or alter any proprietary notices.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
22
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
23
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
24
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
25
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
26
|
+
|
|
27
|
+
For licensing inquiries, contact: edivanteixeira@gmail.com
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rustpdf
|