folio-pdf 0.0.2__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.
Files changed (57) hide show
  1. folio_pdf/__init__.py +285 -0
  2. folio_pdf/color.py +85 -0
  3. folio_pdf/core.py +126 -0
  4. folio_pdf/document.py +1130 -0
  5. folio_pdf/elements/__init__.py +46 -0
  6. folio_pdf/elements/area_break.py +21 -0
  7. folio_pdf/elements/barcode/__init__.py +9 -0
  8. folio_pdf/elements/barcode/barcode.py +128 -0
  9. folio_pdf/elements/barcode/barcode_element.py +95 -0
  10. folio_pdf/elements/columns.py +96 -0
  11. folio_pdf/elements/div.py +461 -0
  12. folio_pdf/elements/flex/__init__.py +9 -0
  13. folio_pdf/elements/flex/flex.py +159 -0
  14. folio_pdf/elements/flex/flex_item.py +141 -0
  15. folio_pdf/elements/float.py +56 -0
  16. folio_pdf/elements/grid.py +489 -0
  17. folio_pdf/elements/heading.py +139 -0
  18. folio_pdf/elements/image/__init__.py +9 -0
  19. folio_pdf/elements/image/image.py +186 -0
  20. folio_pdf/elements/image/image_element.py +130 -0
  21. folio_pdf/elements/line_separator.py +26 -0
  22. folio_pdf/elements/link.py +158 -0
  23. folio_pdf/elements/list.py +192 -0
  24. folio_pdf/elements/paragraph.py +334 -0
  25. folio_pdf/elements/svg/__init__.py +9 -0
  26. folio_pdf/elements/svg/svg.py +88 -0
  27. folio_pdf/elements/svg/svg_element.py +88 -0
  28. folio_pdf/elements/tabbed_line.py +187 -0
  29. folio_pdf/elements/table/__init__.py +10 -0
  30. folio_pdf/elements/table/table.py +183 -0
  31. folio_pdf/elements/table/table_cell.py +219 -0
  32. folio_pdf/elements/table/table_row.py +109 -0
  33. folio_pdf/enums.py +218 -0
  34. folio_pdf/exceptions.py +92 -0
  35. folio_pdf/folio_pdf.pyi +42 -0
  36. folio_pdf/font.py +81 -0
  37. folio_pdf/form_field.py +207 -0
  38. folio_pdf/form_filler.py +103 -0
  39. folio_pdf/forms.py +414 -0
  40. folio_pdf/libs/folio-windows-x86_64.dll +0 -0
  41. folio_pdf/libs/folio.h +781 -0
  42. folio_pdf/merger.py +308 -0
  43. folio_pdf/ocsp_client.py +33 -0
  44. folio_pdf/outline.py +65 -0
  45. folio_pdf/page.py +944 -0
  46. folio_pdf/page_importer.py +66 -0
  47. folio_pdf/py.typed +0 -0
  48. folio_pdf/reader.py +223 -0
  49. folio_pdf/redactor_options.py +109 -0
  50. folio_pdf/run_list.py +231 -0
  51. folio_pdf/signer.py +73 -0
  52. folio_pdf/signer_options.py +128 -0
  53. folio_pdf/tsa_client.py +42 -0
  54. folio_pdf/write_options.py +166 -0
  55. folio_pdf-0.0.2.dist-info/METADATA +81 -0
  56. folio_pdf-0.0.2.dist-info/RECORD +57 -0
  57. folio_pdf-0.0.2.dist-info/WHEEL +4 -0
folio_pdf/__init__.py ADDED
@@ -0,0 +1,285 @@
1
+ """
2
+ Copyright 2026 Gbenga Adeyi and Folio PDF Authors
3
+ SPDX-License-Identifier: Apache-2.0
4
+ """
5
+
6
+ import ctypes as ct
7
+ from io import BytesIO
8
+ from pathlib import Path
9
+
10
+ from folio_pdf.color import Color
11
+ from folio_pdf.document import Document
12
+ from folio_pdf.font import Font
13
+ from folio_pdf.merger import PDFMerger
14
+ from folio_pdf.outline import Outline
15
+ from folio_pdf.page import Page
16
+ from folio_pdf.reader import PDFReader
17
+ from folio_pdf.redactor_options import RedactorOptions
18
+ from folio_pdf.signer_options import SignerOptions
19
+
20
+ from .core import _read_from_obj_buffer, lib
21
+
22
+ __all__ = [
23
+ "Document",
24
+ "Page",
25
+ "Font",
26
+ "PDFMerger",
27
+ "Outline",
28
+ "PDFReader",
29
+ "RedactorOptions",
30
+ "SignerOptions",
31
+ "Color",
32
+ ]
33
+
34
+
35
+ lib.folio_html_to_pdf.argtypes = [ct.c_char_p, ct.c_char_p]
36
+ lib.folio_html_to_pdf.restype = ct.c_int32
37
+
38
+
39
+ def html_to_pdf(html: str, destination: str | Path) -> None:
40
+ """
41
+ Converts an HTML string to a PDF file and writes it to the given path.
42
+
43
+ Args:
44
+ html: the HTML content to render
45
+ destination: the destination file path for the generated PDF
46
+ """
47
+ _destination = destination
48
+ if isinstance(_destination, Path):
49
+ _destination = _destination.as_posix()
50
+ if not _destination.endswith("pdf"):
51
+ _destination += ".pdf"
52
+ # TODO: Handle result code
53
+ lib.folio_html_to_pdf(
54
+ ct.c_char_p(html.encode()), ct.c_char_p(_destination.encode())
55
+ )
56
+
57
+
58
+ lib.folio_html_to_buffer.argtypes = [ct.c_char_p, ct.c_double, ct.c_double]
59
+ lib.folio_html_to_buffer.restype = ct.c_int64
60
+
61
+ lib.folio_buffer_data.restype = ct.c_void_p
62
+
63
+
64
+ def html_to_buffer(html: str, page_width: float, page_height: float) -> BytesIO:
65
+ """
66
+ Converts an HTML string to a PDF with custom page dimensions and returns
67
+ the raw bytes in an in memory buffer.
68
+
69
+ Args:
70
+ html: the HTML content to render
71
+ page_width: page width in points
72
+ page_height: page height in points
73
+
74
+ Returns:
75
+ the generated PDF in a buffer
76
+
77
+ Example:
78
+ ```python
79
+ from folio_pdf import html_to_buffer
80
+ from folio_pdf.enums import PageSizes
81
+
82
+ # Using the dimensions of an A4 page size
83
+ width, height = PageSizes.A4.dimensions
84
+ result = html_to_buffer(
85
+ html='<h1>Folio PDF is awesome!</h1>',
86
+ page_width=width,
87
+ page_height=height)
88
+ ```
89
+ """
90
+ buf = lib.folio_html_to_buffer(
91
+ ct.c_char_p(html.encode()),
92
+ ct.c_double(page_width),
93
+ ct.c_double(page_height),
94
+ )
95
+ data = _read_from_obj_buffer(buf)
96
+ return BytesIO(data)
97
+
98
+
99
+ lib.folio_html_convert.argtypes = [ct.c_char_p, ct.c_double, ct.c_double]
100
+ lib.folio_html_convert.restype = ct.c_uint64
101
+
102
+
103
+ def html_convert(html: str, page_width: float, page_height: float) -> Document:
104
+ """
105
+ Converts an HTML string to a `Document` with custom page dimensions.
106
+
107
+ The caller is responsible for closing the returned document.
108
+
109
+ Args:
110
+ html: the HTML content to render
111
+ page_width: page width in points
112
+ page_height: page height in points
113
+
114
+ Returns:
115
+ a new `Document` representing the converted HTML
116
+
117
+ Example:
118
+ ```python
119
+ from folio_pdf import html_convert
120
+ from folio_pdf.enums import PageSizes
121
+
122
+ # Using the dimensions of a Letter page size
123
+ width, height = PageSizes.LETTER.dimensions
124
+ doc = html_convert(
125
+ html='<h1>Folio PDF is awesome!</h1>',
126
+ page_width=width,
127
+ page_height=height)
128
+ ```
129
+ """
130
+ doc_handle = lib.folio_html_convert(
131
+ ct.c_char_p(html.encode()),
132
+ ct.c_double(page_width),
133
+ ct.c_double(page_height),
134
+ )
135
+ return Document._new_from_handle(doc_handle)
136
+
137
+
138
+ lib.folio_html_parse_css_length.argtypes = [
139
+ ct.c_char_p,
140
+ ct.c_double,
141
+ ct.c_double,
142
+ ]
143
+ lib.folio_html_parse_css_length.restype = ct.c_double
144
+
145
+
146
+ def parse_css_length(s: str, font_size: float, relative_to: float) -> float:
147
+ """Parses a CSS length string and returns its value in points.
148
+
149
+ Args:
150
+ s: the CSS length expression (e.g., `"1in"`, `"16px"`, `"50%"`, `"2em"`)
151
+ font_size: the current font size in points (used for `em`/`rem`)
152
+ relative_to: the reference length in points (used for `%`)
153
+
154
+ Returns:
155
+ the parsed length in points
156
+
157
+
158
+ Example:
159
+ ```python
160
+ pts = parse_css_length("1in", 12, 0) # 72.0
161
+ em = parse_css_length("2em", 16,0) # 32.0
162
+ pct = parse_css_length("50%", 12, 100) # 50.0
163
+ ```
164
+ """
165
+ return lib.folio_html_parse_css_length(
166
+ ct.c_char_p(s.encode()),
167
+ ct.c_double(font_size),
168
+ ct.c_double(relative_to),
169
+ )
170
+
171
+
172
+ lib.folio_sign_pdf.argtypes = [
173
+ ct.c_void_p,
174
+ ct.c_int32,
175
+ ct.c_int64,
176
+ ]
177
+ lib.folio_sign_pdf.restype = ct.c_int64
178
+
179
+
180
+ def sign_pdf(pdf_data: bytes, opts: SignerOptions):
181
+ buf = lib.folio_sign_pdf(
182
+ ct.c_char_p(pdf_data), ct.c_int32(len(pdf_data)), opts._handle
183
+ )
184
+ return _read_from_obj_buffer(buf)
185
+
186
+
187
+ def redact_text(reader: PDFReader, targets: list[str], opts: RedactorOptions) -> bytes:
188
+ """
189
+ Redacts every occurrence of each target string in the given PDF reader.
190
+
191
+ Args:
192
+ reader: source PDF reader
193
+ targets: literal strings to match and redact
194
+ opts: redaction appearance options
195
+
196
+ Returns:
197
+ the redacted PDF bytes
198
+ """
199
+ CharPArray = ct.c_char_p * len(targets)
200
+
201
+ lib.folio_redact_text.argtypes = [
202
+ ct.c_uint64,
203
+ CharPArray,
204
+ ct.c_int32,
205
+ ct.c_uint64,
206
+ ]
207
+ lib.folio_redact_text.restype = ct.c_uint64
208
+
209
+ buf = lib.folio_redact_text(
210
+ reader._handle, CharPArray(targets), ct.c_int32(len(targets)), opts._handle
211
+ )
212
+ return _read_from_obj_buffer(buf)
213
+
214
+
215
+ lib.folio_redact_pattern.argtypes = [ct.c_uint64, ct.c_char_p, ct.c_uint64]
216
+ lib.folio_redact_pattern.restype = ct.c_uint64
217
+
218
+
219
+ def redact_pattern(reader: PDFReader, pattern: str, opts: RedactorOptions):
220
+ """
221
+ Redacts every match of the given regular expression pattern.
222
+
223
+ Args:
224
+ reader: source PDF reader
225
+ pattern: a regular expression used to locate redaction targets
226
+ opts: redaction appearance options
227
+
228
+ Returns:
229
+ the redacted PDF bytes
230
+ """
231
+ buf = lib.folio_redact_pattern(
232
+ reader._handle, ct.c_char_p(pattern.encode()), opts._handle
233
+ )
234
+ return _read_from_obj_buffer(buf)
235
+
236
+
237
+ def redact_regions(
238
+ reader: PDFReader,
239
+ pages: list[int],
240
+ x1s: list[float],
241
+ y1s: list[float],
242
+ x2s: list[float],
243
+ y2s: list[float],
244
+ opts: RedactorOptions,
245
+ ): # TODO: Make this more python friendly
246
+ """
247
+ Redacts the explicit rectangular regions supplied by the caller.
248
+
249
+ Args:
250
+ reader: source PDF reader
251
+ regions: list of page regions to black out
252
+ opts: redaction appearance options
253
+
254
+ Returns:
255
+ the redacted PDF bytes
256
+ """
257
+ assert len(x1s) == len(y1s) == len(x2s) == len(y2s), (
258
+ "The lists x1s,y1s,x2s & y2s must all be of the same length"
259
+ )
260
+ Int32Array = ct.c_int32 * len(pages)
261
+ DoubleArray = ct.c_double * len(x1s)
262
+
263
+ lib.folio_redact_regions.argtypes = [
264
+ ct.c_uint64,
265
+ Int32Array,
266
+ DoubleArray,
267
+ DoubleArray,
268
+ DoubleArray,
269
+ DoubleArray,
270
+ ct.c_int32,
271
+ ct.c_uint64,
272
+ ]
273
+ lib.folio_redact_regions.restype = ct.c_uint64
274
+
275
+ buf = lib.folio_redact_regions(
276
+ reader._handle,
277
+ Int32Array(pages),
278
+ DoubleArray(x1s),
279
+ DoubleArray(y1s),
280
+ DoubleArray(x2s),
281
+ DoubleArray(y2s),
282
+ ct.c_int32(len(x1s)),
283
+ opts._handle,
284
+ )
285
+ return _read_from_obj_buffer(buf)
folio_pdf/color.py ADDED
@@ -0,0 +1,85 @@
1
+ from dataclasses import dataclass
2
+
3
+ from folio_pdf.enums import Colors
4
+
5
+
6
+ @dataclass
7
+ class Color:
8
+ """
9
+ Represents an RGB color with each channel in the range `0.0 - 1.0`.
10
+
11
+ Common colors are available via the `Colors` enum that can be used to call
12
+ the `from_named` class method. New colors can be created from
13
+ normalized floats from instantiating the color class, 8-bit integers via `from_rgb`
14
+ class method, or a CSS hex string via `from_hex` class method.
15
+
16
+ Examples:
17
+ ```python
18
+ from folio_pdf import Color
19
+ from folio_pdf.enums import Colors
20
+
21
+ red = Color.from_named(Colors.RED)
22
+ blue = Color.from_named('blue')
23
+ green = Color.from_rgb(0,255,0)
24
+ black = Color.from_hex("#000000") # with `#` prefix
25
+ white = Color.from_hex("ffffff")
26
+ ```
27
+ """
28
+
29
+ r: float
30
+ g: float
31
+ b: float
32
+
33
+ @classmethod
34
+ def from_named(cls, color: str | Colors):
35
+ _color = color
36
+ if isinstance(_color, str):
37
+ _color = Colors(_color.lower())
38
+ match _color:
39
+ case Colors.BLACK:
40
+ return cls(0, 0, 0)
41
+ case Colors.WHITE:
42
+ return cls(1, 1, 1)
43
+ case Colors.RED:
44
+ return cls(1, 0, 0)
45
+ case Colors.GREEN:
46
+ return cls(0, 1, 0)
47
+ case Colors.BLUE:
48
+ return cls(0, 0, 1)
49
+ case Colors.GRAY:
50
+ return cls(0.5, 0.5, 0.5)
51
+ case Colors.LIGHT_GRAY:
52
+ return cls(0.75, 0.75, 0.75)
53
+ case Colors.DARK_GRAY:
54
+ return cls(0.25, 0.25, 0.25)
55
+ case Colors.NAVY:
56
+ return cls(0, 0, 0.5)
57
+ case Colors.TEAL:
58
+ return cls(0, 0.5, 0.5)
59
+ case Colors.ORANGE:
60
+ return cls(1, 0.65, 0)
61
+ case Colors.PURPLE:
62
+ return cls(0.5, 0, 0.5)
63
+ case Colors.YELLOW:
64
+ return cls(1, 1, 0)
65
+ case Colors.CYAN:
66
+ return cls(0, 1, 1)
67
+ case Colors.MAGENTA:
68
+ return cls(1, 0, 1)
69
+ case Colors.BROWN:
70
+ return cls(0.6, 0.3, 0)
71
+ case Colors.PINK:
72
+ return cls(1, 0.75, 0.8)
73
+
74
+ @classmethod
75
+ def from_rgb(cls, r: float, g: float, b: float):
76
+ return cls(r / 255, g / 255, b / 255)
77
+
78
+ @classmethod
79
+ def from_hex(cls, hex: str):
80
+ _hex = hex[1:] if hex.startswith("#") else hex
81
+ return cls.from_rgb(
82
+ int(_hex[:2], 16),
83
+ int(_hex[2:4], 16),
84
+ int(_hex[4:], 16),
85
+ )
folio_pdf/core.py ADDED
@@ -0,0 +1,126 @@
1
+ """
2
+ Copyright 2026 Gbenga Adeyi and Folio PDF Authors
3
+ SPDX-License-Identifier: Apache-2.0
4
+ """
5
+
6
+ import ctypes as ct
7
+ import platform
8
+ from abc import ABC, abstractmethod
9
+ from pathlib import Path
10
+ from typing import Callable
11
+
12
+ from folio_pdf.enums import ErrorCodes
13
+
14
+ THIS_FILE_PATH = Path(__file__).parent
15
+
16
+
17
+ def _load_lib() -> ct.CDLL:
18
+ os_name = platform.system()
19
+ arch = platform.machine().lower()
20
+
21
+ arch_aliases = {
22
+ "amd64": "x86_64",
23
+ "x86_64": "x86_64",
24
+ "arm64": "aarch64",
25
+ "aarch64": "aarch64",
26
+ }
27
+
28
+ arch = arch_aliases.get(arch, arch)
29
+
30
+ extensions = {
31
+ "Windows": "dll",
32
+ "Linux": "so",
33
+ "Darwin": "dylib",
34
+ }
35
+
36
+ prefixes = {
37
+ "Windows": "",
38
+ "Linux": "lib",
39
+ "Darwin": "lib",
40
+ }
41
+
42
+ if os_name not in extensions:
43
+ raise RuntimeError(f"Unsupported OS: {os_name} Arch: {arch}")
44
+
45
+ filename = (
46
+ f"{prefixes[os_name]}folio-{os_name.lower()}-{arch}.{extensions[os_name]}"
47
+ )
48
+
49
+ lib_path = THIS_FILE_PATH / "libs" / filename
50
+
51
+ if not lib_path.exists():
52
+ raise RuntimeError(f"folio shared library not found at: {lib_path}")
53
+
54
+ return ct.CDLL(str(lib_path))
55
+
56
+
57
+ lib = _load_lib()
58
+
59
+ # Core
60
+ lib.folio_version.argtypes = []
61
+ lib.folio_version.restype = ct.c_char_p
62
+
63
+ lib.folio_last_error.argtypes = []
64
+ lib.folio_last_error.restype = ct.c_char_p
65
+
66
+ # Buffer
67
+ lib.folio_buffer_data.argtypes = [ct.c_int64]
68
+ lib.folio_buffer_data.restype = ct.c_void_p
69
+
70
+ lib.folio_buffer_len.argtypes = [ct.c_int64]
71
+ lib.folio_buffer_len.restype = ct.c_int32
72
+
73
+ lib.folio_buffer_free.argtypes = [ct.c_int64]
74
+ lib.folio_buffer_free.restype = None
75
+
76
+
77
+ def _with_error_handling(exception: type[Exception]):
78
+ def decorator(func: Callable):
79
+ def wrapper(self, *args, **kwargs):
80
+ res_code = func(self, *args, **kwargs)
81
+ err = ErrorCodes(res_code)
82
+ if err != ErrorCodes.OK:
83
+ msg_bytes = lib.folio_last_error()
84
+ raise exception(str(msg_bytes))
85
+ return self
86
+
87
+ return wrapper
88
+
89
+ return decorator
90
+
91
+
92
+ def _read_from_obj_buffer(buf: int):
93
+ size = lib.folio_buffer_len(buf)
94
+ ptr = lib.folio_buffer_data(buf)
95
+ data = ct.string_at(ptr, size)
96
+ lib.folio_buffer_free(buf)
97
+ return data
98
+
99
+
100
+ class AbstractFolioObject(ABC):
101
+ _requires_close: bool
102
+
103
+ @property
104
+ @abstractmethod
105
+ def _handle(self) -> ct.c_uint64:
106
+ """Returns the native object handle"""
107
+ ...
108
+
109
+ def close(self):
110
+ err_msg = (
111
+ "close method has not been implemented for folio object"
112
+ if self._requires_close
113
+ else "folio object does not require close"
114
+ )
115
+ raise NotImplementedError(err_msg)
116
+
117
+ @staticmethod
118
+ def _read_from_obj_buffer(buf: int):
119
+ return _read_from_obj_buffer(buf)
120
+
121
+ def __enter__(self):
122
+ return self
123
+
124
+ def __exit__(self, *_args, **_kwargs):
125
+ if self._requires_close:
126
+ self.close()