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.
- shinobitools-0.1.0/LICENSE +21 -0
- shinobitools-0.1.0/PKG-INFO +126 -0
- shinobitools-0.1.0/README.md +105 -0
- shinobitools-0.1.0/pyproject.toml +33 -0
- shinobitools-0.1.0/setup.cfg +4 -0
- shinobitools-0.1.0/shinobitools/__init__.py +200 -0
- shinobitools-0.1.0/shinobitools.egg-info/PKG-INFO +126 -0
- shinobitools-0.1.0/shinobitools.egg-info/SOURCES.txt +9 -0
- shinobitools-0.1.0/shinobitools.egg-info/dependency_links.txt +1 -0
- shinobitools-0.1.0/shinobitools.egg-info/top_level.txt +1 -0
- shinobitools-0.1.0/tests/test_client.py +82 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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")
|