pdfops 0.1.0__py3-none-any.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.
- pdfops/__init__.py +207 -0
- pdfops/py.typed +0 -0
- pdfops-0.1.0.dist-info/METADATA +120 -0
- pdfops-0.1.0.dist-info/RECORD +6 -0
- pdfops-0.1.0.dist-info/WHEEL +4 -0
- pdfops-0.1.0.dist-info/licenses/LICENSE +21 -0
pdfops/__init__.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""pdfops — Python client for the PDFops API (https://pdfops.dev).
|
|
2
|
+
|
|
3
|
+
Zero dependencies: standard-library ``urllib`` only, so it runs anywhere
|
|
4
|
+
Python does — Lambda, Cloud Functions, Airflow tasks, a cron, a notebook.
|
|
5
|
+
Errors carry the API's structured ``{"error", "details"}`` body via
|
|
6
|
+
:class:`PdfOpsError`.
|
|
7
|
+
|
|
8
|
+
from pdfops import PdfOps
|
|
9
|
+
|
|
10
|
+
client = PdfOps(api_key="pdfops_live_...") # or PdfOps() for the keyless trial
|
|
11
|
+
fields = client.inspect("blank.pdf")["fillTemplate"]
|
|
12
|
+
fields["Name"] = "Ada Lovelace"
|
|
13
|
+
open("filled.pdf", "wb").write(client.fill_form("blank.pdf", fields, flatten=True))
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import uuid
|
|
21
|
+
from pathlib import PurePath
|
|
22
|
+
from typing import IO, Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union
|
|
23
|
+
from urllib import error as urlerror
|
|
24
|
+
from urllib import request as urlrequest
|
|
25
|
+
|
|
26
|
+
__all__ = ["PdfOps", "PdfOpsError", "PdfInput", "__version__"]
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
#: A PDF given as raw bytes, a filesystem path, or a binary file object.
|
|
30
|
+
PdfInput = Union[bytes, bytearray, memoryview, str, "os.PathLike[str]", IO[bytes]]
|
|
31
|
+
|
|
32
|
+
#: ``transport(method, url, headers, body) -> (status, headers, body)``.
|
|
33
|
+
#: Injected in tests; the default speaks HTTP through urllib.
|
|
34
|
+
Transport = Callable[[str, str, Mapping[str, str], Optional[bytes]], Tuple[int, Mapping[str, str], bytes]]
|
|
35
|
+
|
|
36
|
+
DEFAULT_BASE_URL = "https://pdfops.dev"
|
|
37
|
+
USER_AGENT = f"pdfops-python/{__version__}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PdfOpsError(Exception):
|
|
41
|
+
"""Structured API error. ``code`` is the API's stable ``error`` slug."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, status: int, code: str, details: str) -> None:
|
|
44
|
+
super().__init__(f"{code}: {details}")
|
|
45
|
+
self.status = status
|
|
46
|
+
self.code = code
|
|
47
|
+
self.details = details
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _read_pdf(pdf: PdfInput) -> bytes:
|
|
51
|
+
if isinstance(pdf, (bytes, bytearray, memoryview)):
|
|
52
|
+
return bytes(pdf)
|
|
53
|
+
if isinstance(pdf, (str, PurePath)) or hasattr(pdf, "__fspath__"):
|
|
54
|
+
with open(os.fspath(pdf), "rb") as fh: # type: ignore[arg-type]
|
|
55
|
+
return fh.read()
|
|
56
|
+
if hasattr(pdf, "read"):
|
|
57
|
+
data = pdf.read()
|
|
58
|
+
if isinstance(data, str):
|
|
59
|
+
raise TypeError("PDF file objects must be opened in binary mode ('rb')")
|
|
60
|
+
return bytes(data)
|
|
61
|
+
raise TypeError(f"unsupported PDF input type: {type(pdf).__name__}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _multipart(parts: Sequence[Tuple[str, Optional[str], bytes, str]]) -> Tuple[str, bytes]:
|
|
65
|
+
"""Encode ``(name, filename, data, content_type)`` parts; returns (content-type, body)."""
|
|
66
|
+
boundary = f"----pdfops-{uuid.uuid4().hex}"
|
|
67
|
+
out = bytearray()
|
|
68
|
+
for name, filename, data, ctype in parts:
|
|
69
|
+
out += f"--{boundary}\r\n".encode()
|
|
70
|
+
disp = f'form-data; name="{name}"'
|
|
71
|
+
if filename is not None:
|
|
72
|
+
disp += f'; filename="{filename}"'
|
|
73
|
+
out += f"Content-Disposition: {disp}\r\n".encode()
|
|
74
|
+
out += f"Content-Type: {ctype}\r\n\r\n".encode()
|
|
75
|
+
out += data
|
|
76
|
+
out += b"\r\n"
|
|
77
|
+
out += f"--{boundary}--\r\n".encode()
|
|
78
|
+
return f"multipart/form-data; boundary={boundary}", bytes(out)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _urllib_transport(timeout: float) -> Transport:
|
|
82
|
+
def send(method: str, url: str, headers: Mapping[str, str], body: Optional[bytes]) -> Tuple[int, Mapping[str, str], bytes]:
|
|
83
|
+
req = urlrequest.Request(url, data=body, method=method, headers=dict(headers))
|
|
84
|
+
try:
|
|
85
|
+
with urlrequest.urlopen(req, timeout=timeout) as res: # noqa: S310 — fixed https origin
|
|
86
|
+
return res.status, dict(res.headers.items()), res.read()
|
|
87
|
+
except urlerror.HTTPError as e:
|
|
88
|
+
return e.code, dict(e.headers.items()), e.read()
|
|
89
|
+
|
|
90
|
+
return send
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PdfOps:
|
|
94
|
+
"""Client for the PDFops API.
|
|
95
|
+
|
|
96
|
+
:param api_key: key from https://pdfops.dev/pricing (free tier: 250 req/mo).
|
|
97
|
+
Omit for keyless trial calls (100 req/IP/mo).
|
|
98
|
+
:param base_url: API origin override (testing).
|
|
99
|
+
:param client_tag: integration identifier sent as ``X-Pdfops-Client`` for
|
|
100
|
+
anonymous usage attribution. Defaults to ``"py"``; set it when embedding
|
|
101
|
+
this client inside another tool (e.g. ``"airflow"``).
|
|
102
|
+
:param timeout: per-request timeout in seconds.
|
|
103
|
+
:param transport: custom transport (tests / instrumentation).
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
api_key: Optional[str] = None,
|
|
109
|
+
*,
|
|
110
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
111
|
+
client_tag: Optional[str] = "py",
|
|
112
|
+
timeout: float = 60.0,
|
|
113
|
+
transport: Optional[Transport] = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
self.api_key = api_key or os.environ.get("PDFOPS_API_KEY") or None
|
|
116
|
+
self.base_url = base_url.rstrip("/")
|
|
117
|
+
self.client_tag = client_tag
|
|
118
|
+
self._send = transport or _urllib_transport(timeout)
|
|
119
|
+
|
|
120
|
+
# ── plumbing ────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
def _headers(self, extra: Optional[Mapping[str, str]] = None) -> Dict[str, str]:
|
|
123
|
+
h: Dict[str, str] = {"User-Agent": USER_AGENT, "Accept": "application/pdf, application/json"}
|
|
124
|
+
if self.api_key:
|
|
125
|
+
h["X-API-Key"] = self.api_key
|
|
126
|
+
if self.client_tag:
|
|
127
|
+
h["X-Pdfops-Client"] = self.client_tag
|
|
128
|
+
if extra:
|
|
129
|
+
h.update(extra)
|
|
130
|
+
return h
|
|
131
|
+
|
|
132
|
+
def _request(self, method: str, path: str, headers: Mapping[str, str], body: Optional[bytes]) -> bytes:
|
|
133
|
+
status, _res_headers, data = self._send(method, f"{self.base_url}{path}", headers, body)
|
|
134
|
+
if 200 <= status < 300:
|
|
135
|
+
return data
|
|
136
|
+
code, details = f"http_{status}", (data.decode("utf-8", "replace") or f"HTTP {status}")
|
|
137
|
+
try:
|
|
138
|
+
parsed = json.loads(data.decode("utf-8"))
|
|
139
|
+
if isinstance(parsed, dict):
|
|
140
|
+
code = str(parsed.get("error") or code)
|
|
141
|
+
details = str(parsed.get("details") or details)
|
|
142
|
+
except ValueError:
|
|
143
|
+
pass # non-JSON error body — keep the status fallback
|
|
144
|
+
raise PdfOpsError(status, code, details)
|
|
145
|
+
|
|
146
|
+
def _post_pdf(self, path: str, parts: Sequence[Tuple[str, Optional[str], bytes, str]]) -> bytes:
|
|
147
|
+
ctype, body = _multipart(parts)
|
|
148
|
+
return self._request("POST", path, self._headers({"Content-Type": ctype}), body)
|
|
149
|
+
|
|
150
|
+
# ── API ─────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
def inspect(self, pdf: PdfInput) -> Dict[str, Any]:
|
|
153
|
+
"""List a PDF's AcroForm fields plus a paste-ready ``fillTemplate``.
|
|
154
|
+
|
|
155
|
+
Returns ``{"count", "hasXFA", "fields": [...], "fillTemplate": {...}}``.
|
|
156
|
+
A PDF with no form returns ``count == 0`` — not an error.
|
|
157
|
+
"""
|
|
158
|
+
data = self._post_pdf("/api/inspect", [("pdf", "input.pdf", _read_pdf(pdf), "application/pdf")])
|
|
159
|
+
return json.loads(data.decode("utf-8"))
|
|
160
|
+
|
|
161
|
+
def fill_form(self, pdf: PdfInput, fields: Mapping[str, str], *, flatten: bool = False) -> bytes:
|
|
162
|
+
"""Fill AcroForm fields; returns the filled PDF bytes.
|
|
163
|
+
|
|
164
|
+
Field names must exist in the PDF — use :meth:`inspect` to discover
|
|
165
|
+
them. Checkbox values are the strings ``"true"``/``"false"``; choice
|
|
166
|
+
fields take one of their options. ``flatten=True`` bakes values into
|
|
167
|
+
the page content and drops the form so fields are no longer editable.
|
|
168
|
+
"""
|
|
169
|
+
parts: List[Tuple[str, Optional[str], bytes, str]] = [
|
|
170
|
+
("pdf", "input.pdf", _read_pdf(pdf), "application/pdf"),
|
|
171
|
+
("fields", None, json.dumps(dict(fields)).encode("utf-8"), "application/json"),
|
|
172
|
+
]
|
|
173
|
+
if flatten:
|
|
174
|
+
parts.append(("flatten", None, b"true", "text/plain"))
|
|
175
|
+
return self._post_pdf("/api/fill-form", parts)
|
|
176
|
+
|
|
177
|
+
def merge(self, pdfs: Sequence[PdfInput]) -> bytes:
|
|
178
|
+
"""Merge two or more PDFs, in order; returns the combined PDF bytes."""
|
|
179
|
+
if len(pdfs) < 2:
|
|
180
|
+
raise PdfOpsError(400, "too_few_pdfs", "merge needs at least 2 PDFs")
|
|
181
|
+
parts = [("pdf", f"part-{i}.pdf", _read_pdf(p), "application/pdf") for i, p in enumerate(pdfs)]
|
|
182
|
+
return self._post_pdf("/api/merge", parts)
|
|
183
|
+
|
|
184
|
+
def invoice(self, request: Mapping[str, Any]) -> bytes:
|
|
185
|
+
"""Generate a complete invoice PDF from structured data.
|
|
186
|
+
|
|
187
|
+
Deterministic: the same request produces byte-identical output, so it
|
|
188
|
+
is safe to re-render from webhooks and retries. Keys: ``from``, ``to``
|
|
189
|
+
(string or ``{"name", "lines"}``), ``items`` (``description``,
|
|
190
|
+
``unit_price``, optional ``quantity``), optional ``invoice_number``,
|
|
191
|
+
``date``, ``due``, ``currency``, ``tax_rate``, ``notes``.
|
|
192
|
+
"""
|
|
193
|
+
body = json.dumps(dict(request)).encode("utf-8")
|
|
194
|
+
return self._request("POST", "/api/invoice", self._headers({"Content-Type": "application/json"}), body)
|
|
195
|
+
|
|
196
|
+
def usage(self) -> Dict[str, Any]:
|
|
197
|
+
"""Quota state for the configured key: tier, limit, used, remaining, period, resets_at."""
|
|
198
|
+
if not self.api_key:
|
|
199
|
+
raise PdfOpsError(401, "missing_api_key", "usage() requires an api_key")
|
|
200
|
+
data = self._request("GET", "/api/usage", self._headers(), None)
|
|
201
|
+
return json.loads(data.decode("utf-8"))
|
|
202
|
+
|
|
203
|
+
def signup(self, email: str) -> None:
|
|
204
|
+
"""Request a free API key (250 req/mo). The key is emailed, never returned."""
|
|
205
|
+
body = json.dumps({"email": email}).encode("utf-8")
|
|
206
|
+
headers = {"User-Agent": USER_AGENT, "Content-Type": "application/json"}
|
|
207
|
+
self._request("POST", "/api/signup", headers, body)
|
pdfops/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pdfops
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the PDFops API: fill AcroForm PDFs, merge PDFs, inspect form fields, generate invoices. One HTTP call, no native deps, no headless browser.
|
|
5
|
+
Project-URL: Homepage, https://pdfops.dev
|
|
6
|
+
Project-URL: Documentation, https://pdfops.dev/docs
|
|
7
|
+
Project-URL: Fill a PDF in Python, https://pdfops.dev/fill-pdf-in-python
|
|
8
|
+
Project-URL: Source, https://github.com/pdfops/pdfops-python
|
|
9
|
+
Project-URL: Issues, https://github.com/pdfops/pdfops-python/issues
|
|
10
|
+
Author-email: PDFops <hello@pdfops.dev>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: acroform,fill-pdf,invoice,merge-pdf,pdf,pdf-api,pdf-forms,pypdf
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Office/Business
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# pdfops
|
|
32
|
+
|
|
33
|
+
Python client for the [PDFops API](https://pdfops.dev): **fill** AcroForm PDFs, **merge** PDFs, **inspect** form fields, and **generate invoices** with one HTTP call. No native dependencies, no headless browser, nothing to install but this package.
|
|
34
|
+
|
|
35
|
+
Built for the moment `pypdf` / `PyPDFForm` stop cooperating — appearance streams that never render, `NeedAppearances` flags, encrypted government blanks, checkboxes that will not tick — and for runtimes where a PDF toolchain does not fit: Lambda, Cloud Functions, Airflow tasks, notebooks.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install pdfops
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Zero dependencies (standard-library `urllib`), Python 3.9+, fully typed.
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pdfops import PdfOps
|
|
47
|
+
|
|
48
|
+
client = PdfOps(api_key="pdfops_live_...") # or PdfOps() for the keyless trial (100 req/IP/month)
|
|
49
|
+
|
|
50
|
+
# 1. Discover the field names — you cannot fill fields you cannot name.
|
|
51
|
+
info = client.inspect("blank-w9.pdf")
|
|
52
|
+
print(info["count"], "fields")
|
|
53
|
+
fields = info["fillTemplate"] # paste-ready {name: ""} dict
|
|
54
|
+
|
|
55
|
+
# 2. Fill, optionally flattening so the result is no longer editable.
|
|
56
|
+
fields["Name"] = "Ada Lovelace"
|
|
57
|
+
fields["Agree"] = "true" # checkboxes take "true" / "false"
|
|
58
|
+
pdf_bytes = client.fill_form("blank-w9.pdf", fields, flatten=True)
|
|
59
|
+
open("w9-filled.pdf", "wb").write(pdf_bytes)
|
|
60
|
+
|
|
61
|
+
# 3. Merge, in order.
|
|
62
|
+
packet = client.merge(["cover.pdf", pdf_bytes, "terms.pdf"])
|
|
63
|
+
|
|
64
|
+
# 4. Invoice from data — deterministic, safe to re-render from a webhook.
|
|
65
|
+
invoice = client.invoice({
|
|
66
|
+
"from": "Acme Ltd",
|
|
67
|
+
"to": {"name": "Ada Lovelace", "lines": ["12 Analytical Way", "London"]},
|
|
68
|
+
"items": [{"description": "Consulting", "quantity": 3, "unit_price": 120.0}],
|
|
69
|
+
"invoice_number": "2026-0042",
|
|
70
|
+
"date": "2026-09-05",
|
|
71
|
+
"currency": "GBP",
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
PDF inputs may be `bytes`, a path (`str` / `pathlib.Path`), or a binary file object. Every PDF-producing call returns `bytes`.
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
| Method | Endpoint | Returns |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `inspect(pdf)` | `POST /api/inspect` | `dict` — `count`, `hasXFA`, `fields[]`, `fillTemplate` |
|
|
82
|
+
| `fill_form(pdf, fields, *, flatten=False)` | `POST /api/fill-form` | filled PDF `bytes` |
|
|
83
|
+
| `merge([pdf, pdf, ...])` | `POST /api/merge` | merged PDF `bytes` |
|
|
84
|
+
| `invoice(request)` | `POST /api/invoice` | invoice PDF `bytes` |
|
|
85
|
+
| `usage()` | `GET /api/usage` | `dict` — tier, limit, used, remaining, resets_at |
|
|
86
|
+
| `signup(email)` | `POST /api/signup` | `None` — the key is emailed |
|
|
87
|
+
|
|
88
|
+
Full request/response schemas: [pdfops.dev/docs](https://pdfops.dev/docs) · [OpenAPI](https://pdfops.dev/openapi.json).
|
|
89
|
+
|
|
90
|
+
## Errors
|
|
91
|
+
|
|
92
|
+
Every non-2xx response raises `PdfOpsError` carrying the API's stable error slug:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from pdfops import PdfOps, PdfOpsError
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
client.fill_form("form.pdf", {"Nmae": "typo"})
|
|
99
|
+
except PdfOpsError as e:
|
|
100
|
+
print(e.status, e.code, e.details) # 422 unknown_field No field named Nmae
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Common codes: `unknown_field`, `exceeds_max_length`, `invalid_option`, `encrypted_pdf`, `rate_limited`, `invalid_api_key`.
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
- `PdfOps(api_key=...)` or the `PDFOPS_API_KEY` environment variable. A free key (250 req/month, no card) is one field at [pdfops.dev/pricing](https://pdfops.dev/pricing).
|
|
108
|
+
- `client_tag="..."` sets the `X-Pdfops-Client` header for anonymous usage attribution when you embed this client in another tool. Defaults to `"py"`.
|
|
109
|
+
- `timeout=60.0` seconds per request; `base_url=` overrides the API origin for testing.
|
|
110
|
+
- `transport=` injects a `(method, url, headers, body) -> (status, headers, body)` callable — the test suite uses this; no network needed.
|
|
111
|
+
|
|
112
|
+
## Why an API instead of a library
|
|
113
|
+
|
|
114
|
+
`pdf-lib` and `pypdf` are excellent when the PDF is well-behaved. PDFops exists for the other ones: it renders appearance streams server-side so every viewer shows the values, handles the encrypted-with-empty-password blanks governments publish, flattens deterministically, and is byte-for-byte reproducible — the same request yields the same file. The [compatibility registry](https://pdfops.dev/registry) records which public forms fill cleanly with which toolchain.
|
|
115
|
+
|
|
116
|
+
## Links
|
|
117
|
+
|
|
118
|
+
[Fill a PDF in Python](https://pdfops.dev/fill-pdf-in-python) · [Docs](https://pdfops.dev/docs) · [MCP server for AI agents](https://www.npmjs.com/package/pdfops-mcp) · [TypeScript SDK](https://www.npmjs.com/package/pdfops-sdk) · [n8n node](https://www.npmjs.com/package/n8n-nodes-pdfops) · hello@pdfops.dev
|
|
119
|
+
|
|
120
|
+
MIT © PDFops
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pdfops/__init__.py,sha256=AddfR4yziYcEel-w-t-sXbJVGMAR06dpvWuNjhYzTYY,9592
|
|
2
|
+
pdfops/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
pdfops-0.1.0.dist-info/METADATA,sha256=-8osmGUC78NtU9b8Kbmef1aaR-b0hseXI8xlyYMOTgg,5859
|
|
4
|
+
pdfops-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
5
|
+
pdfops-0.1.0.dist-info/licenses/LICENSE,sha256=XGtTUurHEstLRQPCVmw_mBkQsMx8lvABFO0l6aNHWXM,1076
|
|
6
|
+
pdfops-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PDFops (pdfops.dev)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|