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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+
@@ -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