shinobitools 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShinobiTools
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.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: shinobitools
3
+ Version: 0.1.0
4
+ Summary: Free image, PDF, video and passport-photo APIs. No key, no account, no dependencies.
5
+ Author: ShinobiTools
6
+ License: MIT
7
+ Project-URL: Homepage, https://shinobitools.com
8
+ Project-URL: Documentation, https://bgninja.com/api.html
9
+ Project-URL: Source, https://github.com/ShinobiTools/shinobitools-py
10
+ Keywords: background-removal,remove-background,pdf,pdf-merge,ocr,passport-photo,video-to-gif,vectorize,svg,api-client
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # shinobitools
23
+
24
+ Python client for the free [ShinobiTools](https://shinobitools.com) APIs.
25
+ No API key, no account, no sign-up, and **no dependencies** — it uses nothing but the
26
+ standard library, so installing it pulls in nothing else.
27
+
28
+ ```bash
29
+ pip install shinobitools
30
+ ```
31
+
32
+ ```python
33
+ from shinobitools import remove_background
34
+
35
+ open("cutout.png", "wb").write(remove_background("photo.jpg"))
36
+ ```
37
+
38
+ That is the whole thing. No client object, no configuration, no key to rotate.
39
+
40
+ ## What it can do
41
+
42
+ | Function | What happens | Docs |
43
+ |---|---|---|
44
+ | `remove_background(image, background=None)` | Transparent PNG, or composited onto a hex colour | [bgninja.com/api.html](https://bgninja.com/api.html) |
45
+ | `passport_photo(image, country="eu")` | A compliant passport photo, plus the head height it measured | [passportninja.com/api.html](https://passportninja.com/api.html) |
46
+ | `pdf_merge(a, b, ...)` | One PDF out of several | [scanreviver.com/pdf-api](https://scanreviver.com/pdf-api) |
47
+ | `pdf_compress(pdf, level="good")` | A smaller PDF | [scanreviver.com/pdf-api](https://scanreviver.com/pdf-api) |
48
+ | `ocr_text(pdf)` | The text out of a scanned PDF | [scanreviver.com/pdf-api](https://scanreviver.com/pdf-api) |
49
+ | `video_to_gif(video, fps=12, width=480)` | A GIF | [convert.shinobitools.com/api.html](https://convert.shinobitools.com/api.html) |
50
+ | `video_to_mp3(video)` | The audio track | [convert.shinobitools.com/api.html](https://convert.shinobitools.com/api.html) |
51
+ | `vectorize(image, mode="color")` | An SVG traced from a raster image | [convert.shinobitools.com/api.html](https://convert.shinobitools.com/api.html) |
52
+
53
+ Every function accepts a path, an open file, or raw bytes.
54
+
55
+ ## Examples
56
+
57
+ ```python
58
+ from shinobitools import passport_photo, fetch_result
59
+
60
+ job = passport_photo("portrait.jpg", country="nl")
61
+ print(job["head_mm"], job["head_range_mm"]) # e.g. 34.2 [32.0, 36.0]
62
+ open("passport.png", "wb").write(fetch_result(job))
63
+ ```
64
+
65
+ The passport endpoint hands back the head height it produced and the range the country
66
+ requires, so you can check compliance in your own code instead of trusting the crop.
67
+
68
+ ```python
69
+ from shinobitools import pdf_merge, ocr_text, vectorize
70
+
71
+ open("all.pdf", "wb").write(pdf_merge("a.pdf", "b.pdf", "c.pdf"))
72
+ print(ocr_text("scan.pdf")[:200])
73
+ open("logo.svg", "wb").write(vectorize("logo.png"))
74
+ ```
75
+
76
+ ## When something goes wrong
77
+
78
+ ```python
79
+ from shinobitools import remove_background, ShinobiError
80
+
81
+ try:
82
+ remove_background("notes.txt")
83
+ except ShinobiError as e:
84
+ print(e.status, e.message) # 400 can't read this file (TXT) — please upload a photo…
85
+ ```
86
+
87
+ `ShinobiError` carries the HTTP status and the service's own message. A `429` means you
88
+ already have the maximum number of requests running from your address; wait for one to
89
+ finish and retry the same file. There is no daily cap to back off from.
90
+
91
+ ## Limits
92
+
93
+ These are the free limits, enforced server side. The per-service documentation linked
94
+ above is the authority; this table is a summary.
95
+
96
+ | | |
97
+ |---|---|
98
+ | Background removal | 99 MB, 30 megapixels, 2 running at once per IP |
99
+ | PDF work | 25 MB per file, 30 pages, 1 job at a time, results kept 45 minutes |
100
+ | Passport photos | 99 MB, 3 running at once per IP, results kept 45 minutes |
101
+ | Video and audio | 400 MB |
102
+ | Requests per day | no cap on any of them |
103
+
104
+ Nothing is written to disk on the image endpoints: your file is processed in memory and
105
+ the response is the result.
106
+
107
+ ## Why this is free
108
+
109
+ These services are run by the people who wrote this package, and they stay free because
110
+ enough people find them. The client sends a `src` label so we can see that integrations
111
+ exist at all — please leave it in. If your project lists what it uses, a link back to
112
+ [shinobitools.com](https://shinobitools.com) is the whole business model.
113
+
114
+ Planning to push serious volume through it? Say hello first via any of the contact pages
115
+ linked above. We would rather hear from you than throttle you.
116
+
117
+ ## Tests
118
+
119
+ ```bash
120
+ python3 tests/test_client.py # offline
121
+ LIVE=1 python3 tests/test_client.py # plus one real call
122
+ ```
123
+
124
+ ## Licence
125
+
126
+ MIT.
@@ -0,0 +1,105 @@
1
+ # shinobitools
2
+
3
+ Python client for the free [ShinobiTools](https://shinobitools.com) APIs.
4
+ No API key, no account, no sign-up, and **no dependencies** — it uses nothing but the
5
+ standard library, so installing it pulls in nothing else.
6
+
7
+ ```bash
8
+ pip install shinobitools
9
+ ```
10
+
11
+ ```python
12
+ from shinobitools import remove_background
13
+
14
+ open("cutout.png", "wb").write(remove_background("photo.jpg"))
15
+ ```
16
+
17
+ That is the whole thing. No client object, no configuration, no key to rotate.
18
+
19
+ ## What it can do
20
+
21
+ | Function | What happens | Docs |
22
+ |---|---|---|
23
+ | `remove_background(image, background=None)` | Transparent PNG, or composited onto a hex colour | [bgninja.com/api.html](https://bgninja.com/api.html) |
24
+ | `passport_photo(image, country="eu")` | A compliant passport photo, plus the head height it measured | [passportninja.com/api.html](https://passportninja.com/api.html) |
25
+ | `pdf_merge(a, b, ...)` | One PDF out of several | [scanreviver.com/pdf-api](https://scanreviver.com/pdf-api) |
26
+ | `pdf_compress(pdf, level="good")` | A smaller PDF | [scanreviver.com/pdf-api](https://scanreviver.com/pdf-api) |
27
+ | `ocr_text(pdf)` | The text out of a scanned PDF | [scanreviver.com/pdf-api](https://scanreviver.com/pdf-api) |
28
+ | `video_to_gif(video, fps=12, width=480)` | A GIF | [convert.shinobitools.com/api.html](https://convert.shinobitools.com/api.html) |
29
+ | `video_to_mp3(video)` | The audio track | [convert.shinobitools.com/api.html](https://convert.shinobitools.com/api.html) |
30
+ | `vectorize(image, mode="color")` | An SVG traced from a raster image | [convert.shinobitools.com/api.html](https://convert.shinobitools.com/api.html) |
31
+
32
+ Every function accepts a path, an open file, or raw bytes.
33
+
34
+ ## Examples
35
+
36
+ ```python
37
+ from shinobitools import passport_photo, fetch_result
38
+
39
+ job = passport_photo("portrait.jpg", country="nl")
40
+ print(job["head_mm"], job["head_range_mm"]) # e.g. 34.2 [32.0, 36.0]
41
+ open("passport.png", "wb").write(fetch_result(job))
42
+ ```
43
+
44
+ The passport endpoint hands back the head height it produced and the range the country
45
+ requires, so you can check compliance in your own code instead of trusting the crop.
46
+
47
+ ```python
48
+ from shinobitools import pdf_merge, ocr_text, vectorize
49
+
50
+ open("all.pdf", "wb").write(pdf_merge("a.pdf", "b.pdf", "c.pdf"))
51
+ print(ocr_text("scan.pdf")[:200])
52
+ open("logo.svg", "wb").write(vectorize("logo.png"))
53
+ ```
54
+
55
+ ## When something goes wrong
56
+
57
+ ```python
58
+ from shinobitools import remove_background, ShinobiError
59
+
60
+ try:
61
+ remove_background("notes.txt")
62
+ except ShinobiError as e:
63
+ print(e.status, e.message) # 400 can't read this file (TXT) — please upload a photo…
64
+ ```
65
+
66
+ `ShinobiError` carries the HTTP status and the service's own message. A `429` means you
67
+ already have the maximum number of requests running from your address; wait for one to
68
+ finish and retry the same file. There is no daily cap to back off from.
69
+
70
+ ## Limits
71
+
72
+ These are the free limits, enforced server side. The per-service documentation linked
73
+ above is the authority; this table is a summary.
74
+
75
+ | | |
76
+ |---|---|
77
+ | Background removal | 99 MB, 30 megapixels, 2 running at once per IP |
78
+ | PDF work | 25 MB per file, 30 pages, 1 job at a time, results kept 45 minutes |
79
+ | Passport photos | 99 MB, 3 running at once per IP, results kept 45 minutes |
80
+ | Video and audio | 400 MB |
81
+ | Requests per day | no cap on any of them |
82
+
83
+ Nothing is written to disk on the image endpoints: your file is processed in memory and
84
+ the response is the result.
85
+
86
+ ## Why this is free
87
+
88
+ These services are run by the people who wrote this package, and they stay free because
89
+ enough people find them. The client sends a `src` label so we can see that integrations
90
+ exist at all — please leave it in. If your project lists what it uses, a link back to
91
+ [shinobitools.com](https://shinobitools.com) is the whole business model.
92
+
93
+ Planning to push serious volume through it? Say hello first via any of the contact pages
94
+ linked above. We would rather hear from you than throttle you.
95
+
96
+ ## Tests
97
+
98
+ ```bash
99
+ python3 tests/test_client.py # offline
100
+ LIVE=1 python3 tests/test_client.py # plus one real call
101
+ ```
102
+
103
+ ## Licence
104
+
105
+ MIT.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "shinobitools"
7
+ version = "0.1.0"
8
+ description = "Free image, PDF, video and passport-photo APIs. No key, no account, no dependencies."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "ShinobiTools" }]
13
+ keywords = [
14
+ "background-removal", "remove-background", "pdf", "pdf-merge", "ocr",
15
+ "passport-photo", "video-to-gif", "vectorize", "svg", "api-client",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Multimedia :: Graphics :: Graphics Conversion",
23
+ "Topic :: Utilities",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.urls]
28
+ Homepage = "https://shinobitools.com"
29
+ Documentation = "https://bgninja.com/api.html"
30
+ Source = "https://github.com/ShinobiTools/shinobitools-py"
31
+
32
+ [tool.setuptools.packages.find]
33
+ include = ["shinobitools*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,200 @@
1
+ """Thin Python client for the free ShinobiTools APIs.
2
+
3
+ No API key, no account, no dependencies. Every function takes a path (or bytes) and
4
+ returns bytes or a dict, so you can drop it into a script without thinking about it.
5
+
6
+ from shinobitools import remove_background
7
+ open("out.png", "wb").write(remove_background("photo.jpg"))
8
+
9
+ Docs for the underlying HTTP APIs:
10
+ https://bgninja.com/api.html background removal
11
+ https://scanreviver.com/pdf-api PDF work and OCR
12
+ https://passportninja.com/api.html passport photos
13
+ https://convert.shinobitools.com/api.html video, audio and vector
14
+
15
+ The services are free within published limits and are run by the people who wrote this
16
+ package. If this ends up in something you build, a link back is the whole business model.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import mimetypes
22
+ import os
23
+ import urllib.error
24
+ import urllib.request
25
+ import uuid
26
+
27
+ __version__ = "0.1.0"
28
+ __all__ = [
29
+ "remove_background", "passport_photo", "pdf_merge", "pdf_compress",
30
+ "ocr_text", "video_to_gif", "video_to_mp3", "vectorize",
31
+ "ShinobiError", "SRC",
32
+ ]
33
+
34
+ # Identifies this package to the service. It is the only way we can tell that an
35
+ # integration exists, which is what keeps the APIs worth running. Please leave it.
36
+ SRC = "shinobitools-py"
37
+
38
+ BGNINJA = "https://bgninja.com"
39
+ SCANREVIVER = "https://scanreviver.com"
40
+ PASSPORTNINJA = "https://passportninja.com"
41
+ CONVERT = "https://convert.shinobitools.com"
42
+
43
+ TIMEOUT = 300
44
+
45
+
46
+ class ShinobiError(RuntimeError):
47
+ """The service refused the request. `status` is the HTTP code, `message` its reason."""
48
+
49
+ def __init__(self, status: int, message: str):
50
+ super().__init__(f"HTTP {status}: {message}")
51
+ self.status = status
52
+ self.message = message
53
+
54
+
55
+ def _bytes_van(bestand) -> tuple[bytes, str]:
56
+ """Accepteert een pad, een open bestand of kale bytes. Geeft (inhoud, naam)."""
57
+ if isinstance(bestand, (bytes, bytearray)):
58
+ return bytes(bestand), "upload"
59
+ if hasattr(bestand, "read"):
60
+ inhoud = bestand.read()
61
+ return inhoud, os.path.basename(getattr(bestand, "name", "upload"))
62
+ with open(bestand, "rb") as f:
63
+ return f.read(), os.path.basename(str(bestand))
64
+
65
+
66
+ def _multipart(velden: dict, bestanden: list[tuple[str, bytes, str]]) -> tuple[bytes, str]:
67
+ """Bouwt een multipart/form-data-body met alleen de standaardbibliotheek.
68
+
69
+ Bewust geen `requests`: dit pakket heeft nul afhankelijkheden, en dat is voor een
70
+ kleine client een echte eigenschap — wie het installeert krijgt niets anders mee.
71
+ """
72
+ grens = f"----shinobitools{uuid.uuid4().hex}"
73
+ delen: list[bytes] = []
74
+ for naam, waarde in velden.items():
75
+ if waarde is None:
76
+ continue
77
+ delen.append(
78
+ f"--{grens}\r\nContent-Disposition: form-data; name=\"{naam}\"\r\n\r\n"
79
+ f"{waarde}\r\n".encode()
80
+ )
81
+ for naam, inhoud, bestandsnaam in bestanden:
82
+ soort = mimetypes.guess_type(bestandsnaam)[0] or "application/octet-stream"
83
+ delen.append(
84
+ f"--{grens}\r\nContent-Disposition: form-data; "
85
+ f"name=\"{naam}\"; filename=\"{bestandsnaam}\"\r\n"
86
+ f"Content-Type: {soort}\r\n\r\n".encode()
87
+ )
88
+ delen.append(inhoud)
89
+ delen.append(b"\r\n")
90
+ delen.append(f"--{grens}--\r\n".encode())
91
+ return b"".join(delen), f"multipart/form-data; boundary={grens}"
92
+
93
+
94
+ # Cloudflare weigert urllib's standaard User-Agent met "403 error code: 1010" (gemeten
95
+ # 02-09-2026 op de echte API). Zonder deze kop faalt élke aanroep uit dit pakket, dus hij
96
+ # hoort in de client en niet in de handleiding.
97
+ UA = f"shinobitools-py/{__version__} (+https://shinobitools.com)"
98
+
99
+
100
+ def _post(url: str, velden: dict, bestanden: list[tuple[str, bytes, str]]):
101
+ velden = {"src": SRC, **velden}
102
+ body, soort = _multipart(velden, bestanden)
103
+ req = urllib.request.Request(
104
+ url, data=body, headers={"Content-Type": soort, "User-Agent": UA})
105
+ try:
106
+ with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
107
+ rauw = r.read()
108
+ if "json" in (r.headers.get("Content-Type") or ""):
109
+ return json.loads(rauw.decode())
110
+ return rauw
111
+ except urllib.error.HTTPError as e:
112
+ rauw = e.read().decode(errors="replace")
113
+ try:
114
+ boodschap = json.loads(rauw).get("detail") or json.loads(rauw).get("error") or rauw
115
+ except Exception: # noqa: BLE001
116
+ boodschap = rauw
117
+ raise ShinobiError(e.code, str(boodschap)[:400]) from None
118
+
119
+
120
+ def _get(url: str) -> bytes:
121
+ try:
122
+ req = urllib.request.Request(url, headers={"User-Agent": UA})
123
+ with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
124
+ return r.read()
125
+ except urllib.error.HTTPError as e:
126
+ raise ShinobiError(e.code, e.read().decode(errors="replace")[:400]) from None
127
+
128
+
129
+ # ── bgninja ────────────────────────────────────────────────────────────────────
130
+ def remove_background(image, background: str | None = None) -> bytes:
131
+ """Remove the background from an image. Returns PNG bytes.
132
+
133
+ `background` is a hex colour without the '#', e.g. "ffffff". Leave it out for a
134
+ transparent background.
135
+ """
136
+ inhoud, naam = _bytes_van(image)
137
+ return _post(f"{BGNINJA}/api/remove", {"bg": background}, [("file", inhoud, naam)])
138
+
139
+
140
+ # ── passportninja ──────────────────────────────────────────────────────────────
141
+ def passport_photo(image, country: str = "eu") -> dict:
142
+ """Turn a portrait into a passport photo. Returns the service's JSON.
143
+
144
+ The result includes `head_mm` and `head_range_mm`, so you can check compliance
145
+ yourself. Add `photo_bytes()` on the returned dict via `fetch_result`.
146
+ """
147
+ inhoud, naam = _bytes_van(image)
148
+ return _post(f"{PASSPORTNINJA}/api/photo", {"country": country}, [("file", inhoud, naam)])
149
+
150
+
151
+ def fetch_result(job: dict, key: str = "photo") -> bytes:
152
+ """Download one of the files a passport job produced, e.g. "photo" or "sheet"."""
153
+ pad = job[key]
154
+ return _get(pad if pad.startswith("http") else PASSPORTNINJA + pad)
155
+
156
+
157
+ # ── scanreviver ────────────────────────────────────────────────────────────────
158
+ def pdf_merge(*pdfs) -> bytes:
159
+ """Merge two or more PDFs into one. Returns the merged PDF."""
160
+ if len(pdfs) < 2:
161
+ raise ValueError("merging needs at least two PDFs")
162
+ bestanden = []
163
+ for p in pdfs:
164
+ inhoud, naam = _bytes_van(p)
165
+ bestanden.append(("files", inhoud, naam))
166
+ job = _post(f"{SCANREVIVER}/api/pdf-merge", {}, bestanden)
167
+ return _get(f"{SCANREVIVER}/api/pdf-file/{job['id']}")
168
+
169
+
170
+ def pdf_compress(pdf, level: str = "good") -> bytes:
171
+ """Compress a PDF. `level` is the strength the service accepts, "good" by default."""
172
+ inhoud, naam = _bytes_van(pdf)
173
+ job = _post(f"{SCANREVIVER}/api/pdf-compress", {"level": level}, [("file", inhoud, naam)])
174
+ return _get(f"{SCANREVIVER}/api/pdf-file/{job['id']}")
175
+
176
+
177
+ def ocr_text(pdf) -> str:
178
+ """Read the text out of a scanned PDF. Returns the recognised text."""
179
+ inhoud, naam = _bytes_van(pdf)
180
+ return _post(f"{SCANREVIVER}/api/ocr-text", {}, [("file", inhoud, naam)])["text"]
181
+
182
+
183
+ # ── convert ────────────────────────────────────────────────────────────────────
184
+ def video_to_gif(video, fps: float = 12.0, width: int = 480) -> bytes:
185
+ """Turn a video into a GIF. Returns GIF bytes."""
186
+ inhoud, naam = _bytes_van(video)
187
+ return _post(f"{CONVERT}/api/gif", {"fps": fps, "breedte": width},
188
+ [("file", inhoud, naam)])
189
+
190
+
191
+ def video_to_mp3(video) -> bytes:
192
+ """Pull the audio track out of a video. Returns MP3 bytes."""
193
+ inhoud, naam = _bytes_van(video)
194
+ return _post(f"{CONVERT}/api/audio", {"formaat": "mp3"}, [("file", inhoud, naam)])
195
+
196
+
197
+ def vectorize(image, mode: str = "color") -> bytes:
198
+ """Trace a raster image into an SVG. Returns SVG bytes. Best on flat artwork."""
199
+ inhoud, naam = _bytes_van(image)
200
+ return _post(f"{CONVERT}/api/vectorize", {"modus": mode}, [("file", inhoud, naam)])
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: shinobitools
3
+ Version: 0.1.0
4
+ Summary: Free image, PDF, video and passport-photo APIs. No key, no account, no dependencies.
5
+ Author: ShinobiTools
6
+ License: MIT
7
+ Project-URL: Homepage, https://shinobitools.com
8
+ Project-URL: Documentation, https://bgninja.com/api.html
9
+ Project-URL: Source, https://github.com/ShinobiTools/shinobitools-py
10
+ Keywords: background-removal,remove-background,pdf,pdf-merge,ocr,passport-photo,video-to-gif,vectorize,svg,api-client
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # shinobitools
23
+
24
+ Python client for the free [ShinobiTools](https://shinobitools.com) APIs.
25
+ No API key, no account, no sign-up, and **no dependencies** — it uses nothing but the
26
+ standard library, so installing it pulls in nothing else.
27
+
28
+ ```bash
29
+ pip install shinobitools
30
+ ```
31
+
32
+ ```python
33
+ from shinobitools import remove_background
34
+
35
+ open("cutout.png", "wb").write(remove_background("photo.jpg"))
36
+ ```
37
+
38
+ That is the whole thing. No client object, no configuration, no key to rotate.
39
+
40
+ ## What it can do
41
+
42
+ | Function | What happens | Docs |
43
+ |---|---|---|
44
+ | `remove_background(image, background=None)` | Transparent PNG, or composited onto a hex colour | [bgninja.com/api.html](https://bgninja.com/api.html) |
45
+ | `passport_photo(image, country="eu")` | A compliant passport photo, plus the head height it measured | [passportninja.com/api.html](https://passportninja.com/api.html) |
46
+ | `pdf_merge(a, b, ...)` | One PDF out of several | [scanreviver.com/pdf-api](https://scanreviver.com/pdf-api) |
47
+ | `pdf_compress(pdf, level="good")` | A smaller PDF | [scanreviver.com/pdf-api](https://scanreviver.com/pdf-api) |
48
+ | `ocr_text(pdf)` | The text out of a scanned PDF | [scanreviver.com/pdf-api](https://scanreviver.com/pdf-api) |
49
+ | `video_to_gif(video, fps=12, width=480)` | A GIF | [convert.shinobitools.com/api.html](https://convert.shinobitools.com/api.html) |
50
+ | `video_to_mp3(video)` | The audio track | [convert.shinobitools.com/api.html](https://convert.shinobitools.com/api.html) |
51
+ | `vectorize(image, mode="color")` | An SVG traced from a raster image | [convert.shinobitools.com/api.html](https://convert.shinobitools.com/api.html) |
52
+
53
+ Every function accepts a path, an open file, or raw bytes.
54
+
55
+ ## Examples
56
+
57
+ ```python
58
+ from shinobitools import passport_photo, fetch_result
59
+
60
+ job = passport_photo("portrait.jpg", country="nl")
61
+ print(job["head_mm"], job["head_range_mm"]) # e.g. 34.2 [32.0, 36.0]
62
+ open("passport.png", "wb").write(fetch_result(job))
63
+ ```
64
+
65
+ The passport endpoint hands back the head height it produced and the range the country
66
+ requires, so you can check compliance in your own code instead of trusting the crop.
67
+
68
+ ```python
69
+ from shinobitools import pdf_merge, ocr_text, vectorize
70
+
71
+ open("all.pdf", "wb").write(pdf_merge("a.pdf", "b.pdf", "c.pdf"))
72
+ print(ocr_text("scan.pdf")[:200])
73
+ open("logo.svg", "wb").write(vectorize("logo.png"))
74
+ ```
75
+
76
+ ## When something goes wrong
77
+
78
+ ```python
79
+ from shinobitools import remove_background, ShinobiError
80
+
81
+ try:
82
+ remove_background("notes.txt")
83
+ except ShinobiError as e:
84
+ print(e.status, e.message) # 400 can't read this file (TXT) — please upload a photo…
85
+ ```
86
+
87
+ `ShinobiError` carries the HTTP status and the service's own message. A `429` means you
88
+ already have the maximum number of requests running from your address; wait for one to
89
+ finish and retry the same file. There is no daily cap to back off from.
90
+
91
+ ## Limits
92
+
93
+ These are the free limits, enforced server side. The per-service documentation linked
94
+ above is the authority; this table is a summary.
95
+
96
+ | | |
97
+ |---|---|
98
+ | Background removal | 99 MB, 30 megapixels, 2 running at once per IP |
99
+ | PDF work | 25 MB per file, 30 pages, 1 job at a time, results kept 45 minutes |
100
+ | Passport photos | 99 MB, 3 running at once per IP, results kept 45 minutes |
101
+ | Video and audio | 400 MB |
102
+ | Requests per day | no cap on any of them |
103
+
104
+ Nothing is written to disk on the image endpoints: your file is processed in memory and
105
+ the response is the result.
106
+
107
+ ## Why this is free
108
+
109
+ These services are run by the people who wrote this package, and they stay free because
110
+ enough people find them. The client sends a `src` label so we can see that integrations
111
+ exist at all — please leave it in. If your project lists what it uses, a link back to
112
+ [shinobitools.com](https://shinobitools.com) is the whole business model.
113
+
114
+ Planning to push serious volume through it? Say hello first via any of the contact pages
115
+ linked above. We would rather hear from you than throttle you.
116
+
117
+ ## Tests
118
+
119
+ ```bash
120
+ python3 tests/test_client.py # offline
121
+ LIVE=1 python3 tests/test_client.py # plus one real call
122
+ ```
123
+
124
+ ## Licence
125
+
126
+ MIT.
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ shinobitools/__init__.py
5
+ shinobitools.egg-info/PKG-INFO
6
+ shinobitools.egg-info/SOURCES.txt
7
+ shinobitools.egg-info/dependency_links.txt
8
+ shinobitools.egg-info/top_level.txt
9
+ tests/test_client.py
@@ -0,0 +1 @@
1
+ shinobitools
@@ -0,0 +1,82 @@
1
+ """Eén controle die faalt zodra de multipart-opbouw stukgaat.
2
+
3
+ Dat is het enige echt niet-triviale stuk in dit pakket: de rest is een URL en een
4
+ JSON-sleutel. Draait zonder net.
5
+
6
+ python3 tests/test_client.py # offline, altijd
7
+ LIVE=1 python3 tests/test_client.py # ook één echte aanroep naar de API
8
+ """
9
+ import os
10
+ import sys
11
+
12
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
13
+
14
+ from shinobitools import SRC, _bytes_van, _multipart, remove_background # noqa: E402
15
+
16
+
17
+ def test_multipart():
18
+ body, soort = _multipart({"src": SRC, "bg": "ffffff", "leeg": None},
19
+ [("file", b"\x89PNG-nep", "kat.png")])
20
+ tekst = body.decode("latin-1")
21
+
22
+ grens = soort.split("boundary=")[1]
23
+ assert soort.startswith("multipart/form-data; "), soort
24
+ # de scheiding moet echt in de body staan, en de body moet erop eindigen
25
+ assert tekst.startswith(f"--{grens}\r\n"), "body begint niet met de scheiding"
26
+ assert tekst.endswith(f"--{grens}--\r\n"), "body sluit niet af met de slot-scheiding"
27
+ # gewone velden
28
+ assert 'name="src"' in tekst and SRC in tekst
29
+ assert 'name="bg"' in tekst and "ffffff" in tekst
30
+ # None-velden horen NIET mee te gaan: anders stuurt de client de tekst "None"
31
+ assert 'name="leeg"' not in tekst, "een leeg veld werd toch meegestuurd"
32
+ # bestand met naam, type en de rauwe bytes
33
+ assert 'name="file"; filename="kat.png"' in tekst
34
+ assert "Content-Type: image/png" in tekst
35
+ assert b"\x89PNG-nep" in body, "de bestandsinhoud staat niet in de body"
36
+ print(" multipart ok")
37
+
38
+
39
+ def test_bytes_van():
40
+ assert _bytes_van(b"abc") == (b"abc", "upload")
41
+ pad = os.path.join(os.path.dirname(__file__), "_proef.txt")
42
+ with open(pad, "wb") as f:
43
+ f.write(b"hallo")
44
+ try:
45
+ assert _bytes_van(pad) == (b"hallo", "_proef.txt")
46
+ with open(pad, "rb") as f:
47
+ assert _bytes_van(f) == (b"hallo", "_proef.txt")
48
+ finally:
49
+ os.remove(pad)
50
+ print(" bytes_van ok")
51
+
52
+
53
+ def test_live():
54
+ """Alleen met LIVE=1: één echte aanroep, zodat 'het werkt' geen aanname is."""
55
+ import io
56
+ import struct
57
+ import zlib
58
+
59
+ def mini_png() -> bytes: # 2x2 rood, zonder Pillow
60
+ def blok(soort, data):
61
+ return (struct.pack(">I", len(data)) + soort + data
62
+ + struct.pack(">I", zlib.crc32(soort + data)))
63
+ rauw = b"".join(b"\x00" + b"\xff\x00\x00" * 2 for _ in range(2))
64
+ return (b"\x89PNG\r\n\x1a\n"
65
+ + blok(b"IHDR", struct.pack(">IIBBBBB", 2, 2, 8, 2, 0, 0, 0))
66
+ + blok(b"IDAT", zlib.compress(rauw))
67
+ + blok(b"IEND", b""))
68
+
69
+ uit = remove_background(mini_png())
70
+ assert uit[:8] == b"\x89PNG\r\n\x1a\n", "de service gaf geen PNG terug"
71
+ print(f" live ok — {len(uit)} bytes PNG terug")
72
+ _ = io.BytesIO
73
+
74
+
75
+ if __name__ == "__main__":
76
+ test_multipart()
77
+ test_bytes_van()
78
+ if os.environ.get("LIVE"):
79
+ test_live()
80
+ else:
81
+ print(" live overgeslagen (zet LIVE=1)")
82
+ print("alles ok")