webpthumb 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.
- webpthumb-0.1.0/PKG-INFO +142 -0
- webpthumb-0.1.0/README.md +124 -0
- webpthumb-0.1.0/pyproject.toml +33 -0
- webpthumb-0.1.0/setup.cfg +4 -0
- webpthumb-0.1.0/src/webpthumb/__init__.py +15 -0
- webpthumb-0.1.0/src/webpthumb/__main__.py +4 -0
- webpthumb-0.1.0/src/webpthumb/api.py +125 -0
- webpthumb-0.1.0/src/webpthumb/cli.py +321 -0
- webpthumb-0.1.0/src/webpthumb/core.py +245 -0
- webpthumb-0.1.0/src/webpthumb/daemon.py +368 -0
- webpthumb-0.1.0/src/webpthumb/options.py +34 -0
- webpthumb-0.1.0/src/webpthumb/watch.py +229 -0
- webpthumb-0.1.0/src/webpthumb.egg-info/PKG-INFO +142 -0
- webpthumb-0.1.0/src/webpthumb.egg-info/SOURCES.txt +21 -0
- webpthumb-0.1.0/src/webpthumb.egg-info/dependency_links.txt +1 -0
- webpthumb-0.1.0/src/webpthumb.egg-info/entry_points.txt +2 -0
- webpthumb-0.1.0/src/webpthumb.egg-info/requires.txt +10 -0
- webpthumb-0.1.0/src/webpthumb.egg-info/top_level.txt +1 -0
- webpthumb-0.1.0/tests/test_api.py +83 -0
- webpthumb-0.1.0/tests/test_cli.py +69 -0
- webpthumb-0.1.0/tests/test_core.py +122 -0
- webpthumb-0.1.0/tests/test_daemon.py +130 -0
- webpthumb-0.1.0/tests/test_watch.py +110 -0
webpthumb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webpthumb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: WebP thumbnails for images and PDFs, from the CLI, HTTP, or a folder watcher
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: webp,thumbnail,pdf,image,heif,heic,watchdog
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pdfthumb>=0.1.2
|
|
10
|
+
Requires-Dist: image-to-webp[heif]>=0.1.0
|
|
11
|
+
Requires-Dist: watchdog>=4
|
|
12
|
+
Requires-Dist: fastapi>=0.110
|
|
13
|
+
Requires-Dist: uvicorn>=0.27
|
|
14
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
17
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
18
|
+
|
|
19
|
+
# webpthumb
|
|
20
|
+
|
|
21
|
+
WebP thumbnails for images and PDFs. One command covers a one-shot convert, an HTTP endpoint, and a directory watcher.
|
|
22
|
+
|
|
23
|
+
PDFs are rendered by [pdfthumb](https://github.com/stakmi/pdfthumb) (PyMuPDF). Images are resized and encoded by [image-to-webp](https://github.com/stakmi/image-to-webp) (Pillow). The watcher uses [watchdog](https://github.com/gorakhargosh/watchdog). A thumbnail is a single still frame: the first frame of an animated GIF or APNG, or the requested PDF page.
|
|
24
|
+
|
|
25
|
+
Images are only shrunk, to fit the requested width (and height, when you set one). PDFs are rendered at that width. The defaults match pdfthumb: 256 pixels wide, WebP quality 80, page 1.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
Python 3.9 or newer. `install.sh` prefers `python3.12` when it is on `PATH`.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
./install.sh # .venv, editable install, link ~/.local/bin/webpthumb
|
|
33
|
+
./install.sh --dev # also install pytest
|
|
34
|
+
./install.sh --uninstall
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Override the link location with `BIN_DIR` (default `~/.local/bin`). The script uses `sudo` only when that directory is not writable.
|
|
38
|
+
|
|
39
|
+
The default install reads the formats Pillow ships with (PNG, JPEG, GIF, BMP, TIFF, WebP, ICO, and others), HEIF (`.heic`, `.heif`, `.hif`, and AVIF, via pillow-heif), and PDF. Camera RAW and SVG need the optional extras of `image-to-webp` (`pip install "image-to-webp[raw]"` or `[svg]`). SVG also needs the system cairo library. RAW and SVG are not in the Docker image.
|
|
40
|
+
|
|
41
|
+
## CLI
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
webpthumb convert photo.png doc.pdf -o ./thumbnails
|
|
45
|
+
webpthumb convert ./inbox -r -o ./thumbnails -w 512 -q 80
|
|
46
|
+
webpthumb convert doc.pdf -p all # doc_p1.webp, doc_p2.webp, ...
|
|
47
|
+
webpthumb convert doc.pdf --height 300 # fit inside 256x300
|
|
48
|
+
webpthumb convert doc.pdf --crop-top # PDF only: top square
|
|
49
|
+
webpthumb convert doc.pdf -c 0.5 # PDF only: top band, 256x128
|
|
50
|
+
|
|
51
|
+
webpthumb serve --host 127.0.0.1 --port 8080
|
|
52
|
+
|
|
53
|
+
webpthumb watch ./inbox -o ./thumbnails -r
|
|
54
|
+
webpthumb watch ./inbox -o ./thumbnails -r --background
|
|
55
|
+
webpthumb start ./inbox -o ./thumbnails -r
|
|
56
|
+
webpthumb status
|
|
57
|
+
webpthumb stop
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`convert` sends PDFs through `pdfthumb.generate_thumbnails` and images through image-to-webp. Output names are `<name>.webp` for an image or for PDF page 1. Any other page spec uses `<name>_p<N>.webp`. Existing files are skipped unless you pass `--overwrite`.
|
|
61
|
+
|
|
62
|
+
Exit codes: `0` when every file succeeded or was skipped, `1` when a file failed, `2` for bad arguments. `status` exits `1` when the background watcher is not running. `stop` exits `0` when it is already stopped.
|
|
63
|
+
|
|
64
|
+
## HTTP
|
|
65
|
+
|
|
66
|
+
`GET /health` returns `{"status":"ok"}`.
|
|
67
|
+
|
|
68
|
+
`POST /v1/thumbnail` takes one multipart field named `file` and returns `image/webp`. Query parameters: `width`, `height`, `quality`, `page`, `crop_top`. `crop_top` applies only to PDFs. Interactive docs are at `/docs`.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl -sS -D- --output thumb.webp \
|
|
72
|
+
-F "file=@photo.png" \
|
|
73
|
+
"http://127.0.0.1:8080/v1/thumbnail?width=256&quality=80"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A file that is not an image or a PDF, or a PDF that cannot be read, returns `400`. An upload larger than `WEBTHUMB_MAX_UPLOAD_MB` (default 32) returns `413`. The endpoint reads only the upload. It does not accept a filesystem path.
|
|
77
|
+
|
|
78
|
+
## Watch
|
|
79
|
+
|
|
80
|
+
`webpthumb watch` thumbnails files already in the directory, then stays running until SIGINT or SIGTERM. New and updated files are picked up with watchdog. The watcher waits until a file's size stops changing (about half a second) so a copy in progress is not read early.
|
|
81
|
+
|
|
82
|
+
`webpthumb watch --background` and `webpthumb start` do that work in the background and return. `webpthumb status` prints whether that watcher is running. `webpthumb stop` sends SIGTERM and waits for it to exit. A second start is refused while the first is still running. The pid, the directories, and the log live under `WEBTHUMB_RUN_DIR` (`$XDG_STATE_HOME/webpthumb`, or `~/.local/state/webpthumb`). A foreground `watch` is not tracked. Keep `watch` in the foreground when it is the main process of a container.
|
|
83
|
+
|
|
84
|
+
Watched extensions: `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.tif`, `.tiff`, `.webp`, `.ico`, `.heic`, `.heics`, `.heif`, `.heifs`, `.hif`, `.avif`, `.avifs`, `.pdf`. Hidden files and `*.part` files are ignored. The output directory must not sit inside the input directory. With `-r`, the output tree mirrors the input tree.
|
|
85
|
+
|
|
86
|
+
A thumbnail is rewritten when the source is newer than the WebP, or when `--overwrite` is set.
|
|
87
|
+
|
|
88
|
+
`webpthumb serve` starts the same watcher in the background when both `WEBTHUMB_WATCH` and `WEBTHUMB_OUTPUT` are set.
|
|
89
|
+
|
|
90
|
+
## Environment
|
|
91
|
+
|
|
92
|
+
Used when the matching flag or query parameter is omitted.
|
|
93
|
+
|
|
94
|
+
| Variable | Default | Used by |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `WEBTHUMB_WIDTH` | `256` | convert, serve, watch |
|
|
97
|
+
| `WEBTHUMB_HEIGHT` | unset | convert, serve, watch |
|
|
98
|
+
| `WEBTHUMB_QUALITY` | `80` | convert, serve, watch |
|
|
99
|
+
| `WEBTHUMB_PAGES` | `1` | convert, watch |
|
|
100
|
+
| `WEBTHUMB_CROP_TOP` | unset | convert, serve, watch |
|
|
101
|
+
| `WEBTHUMB_OVERWRITE` | off | convert, watch (`1`, `true`, `yes`, `on`) |
|
|
102
|
+
| `WEBTHUMB_RECURSIVE` | off | convert, watch |
|
|
103
|
+
| `WEBTHUMB_JOBS` | CPU count | convert |
|
|
104
|
+
| `WEBTHUMB_OUTPUT` | `./thumbnails` for convert | convert, watch, serve |
|
|
105
|
+
| `WEBTHUMB_WATCH` | unset | serve, with `WEBTHUMB_OUTPUT` |
|
|
106
|
+
| `WEBTHUMB_HOST` | `127.0.0.1` | serve (the image sets `0.0.0.0`) |
|
|
107
|
+
| `WEBTHUMB_PORT` | `8080` | serve |
|
|
108
|
+
| `WEBTHUMB_MAX_UPLOAD_MB` | `32` | serve |
|
|
109
|
+
| `WEBTHUMB_RUN_DIR` | `~/.local/state/webpthumb` | start, stop, status |
|
|
110
|
+
|
|
111
|
+
## Docker
|
|
112
|
+
|
|
113
|
+
The image is `python:3.12-slim` plus this package. PyMuPDF carries its own MuPDF build, so poppler and ImageMagick are not installed.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
docker build -t webpthumb .
|
|
117
|
+
docker run --rm -p 8080:8080 webpthumb
|
|
118
|
+
|
|
119
|
+
# API and watcher together
|
|
120
|
+
docker compose up --build
|
|
121
|
+
|
|
122
|
+
# watcher only
|
|
123
|
+
docker run --rm -v "$PWD/in:/data/in" -v "$PWD/out:/data/out" \
|
|
124
|
+
webpthumb watch /data/in -o /data/out -r
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`docker compose up` publishes port 8080 and mounts `./in` and `./out`. Drop a PNG, HEIC, or PDF into `./in` and the thumbnail appears in `./out`.
|
|
128
|
+
|
|
129
|
+
## Tests
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pytest
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Library
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from webpthumb import ThumbOptions, make_thumbnail, write_thumbnail
|
|
139
|
+
|
|
140
|
+
webp = make_thumbnail(open("doc.pdf", "rb").read(), "doc.pdf", ThumbOptions(width=256))
|
|
141
|
+
paths = write_thumbnail("photo.png", "thumbnails", ThumbOptions(width=256, quality=80))
|
|
142
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# webpthumb
|
|
2
|
+
|
|
3
|
+
WebP thumbnails for images and PDFs. One command covers a one-shot convert, an HTTP endpoint, and a directory watcher.
|
|
4
|
+
|
|
5
|
+
PDFs are rendered by [pdfthumb](https://github.com/stakmi/pdfthumb) (PyMuPDF). Images are resized and encoded by [image-to-webp](https://github.com/stakmi/image-to-webp) (Pillow). The watcher uses [watchdog](https://github.com/gorakhargosh/watchdog). A thumbnail is a single still frame: the first frame of an animated GIF or APNG, or the requested PDF page.
|
|
6
|
+
|
|
7
|
+
Images are only shrunk, to fit the requested width (and height, when you set one). PDFs are rendered at that width. The defaults match pdfthumb: 256 pixels wide, WebP quality 80, page 1.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Python 3.9 or newer. `install.sh` prefers `python3.12` when it is on `PATH`.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
./install.sh # .venv, editable install, link ~/.local/bin/webpthumb
|
|
15
|
+
./install.sh --dev # also install pytest
|
|
16
|
+
./install.sh --uninstall
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Override the link location with `BIN_DIR` (default `~/.local/bin`). The script uses `sudo` only when that directory is not writable.
|
|
20
|
+
|
|
21
|
+
The default install reads the formats Pillow ships with (PNG, JPEG, GIF, BMP, TIFF, WebP, ICO, and others), HEIF (`.heic`, `.heif`, `.hif`, and AVIF, via pillow-heif), and PDF. Camera RAW and SVG need the optional extras of `image-to-webp` (`pip install "image-to-webp[raw]"` or `[svg]`). SVG also needs the system cairo library. RAW and SVG are not in the Docker image.
|
|
22
|
+
|
|
23
|
+
## CLI
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
webpthumb convert photo.png doc.pdf -o ./thumbnails
|
|
27
|
+
webpthumb convert ./inbox -r -o ./thumbnails -w 512 -q 80
|
|
28
|
+
webpthumb convert doc.pdf -p all # doc_p1.webp, doc_p2.webp, ...
|
|
29
|
+
webpthumb convert doc.pdf --height 300 # fit inside 256x300
|
|
30
|
+
webpthumb convert doc.pdf --crop-top # PDF only: top square
|
|
31
|
+
webpthumb convert doc.pdf -c 0.5 # PDF only: top band, 256x128
|
|
32
|
+
|
|
33
|
+
webpthumb serve --host 127.0.0.1 --port 8080
|
|
34
|
+
|
|
35
|
+
webpthumb watch ./inbox -o ./thumbnails -r
|
|
36
|
+
webpthumb watch ./inbox -o ./thumbnails -r --background
|
|
37
|
+
webpthumb start ./inbox -o ./thumbnails -r
|
|
38
|
+
webpthumb status
|
|
39
|
+
webpthumb stop
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`convert` sends PDFs through `pdfthumb.generate_thumbnails` and images through image-to-webp. Output names are `<name>.webp` for an image or for PDF page 1. Any other page spec uses `<name>_p<N>.webp`. Existing files are skipped unless you pass `--overwrite`.
|
|
43
|
+
|
|
44
|
+
Exit codes: `0` when every file succeeded or was skipped, `1` when a file failed, `2` for bad arguments. `status` exits `1` when the background watcher is not running. `stop` exits `0` when it is already stopped.
|
|
45
|
+
|
|
46
|
+
## HTTP
|
|
47
|
+
|
|
48
|
+
`GET /health` returns `{"status":"ok"}`.
|
|
49
|
+
|
|
50
|
+
`POST /v1/thumbnail` takes one multipart field named `file` and returns `image/webp`. Query parameters: `width`, `height`, `quality`, `page`, `crop_top`. `crop_top` applies only to PDFs. Interactive docs are at `/docs`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
curl -sS -D- --output thumb.webp \
|
|
54
|
+
-F "file=@photo.png" \
|
|
55
|
+
"http://127.0.0.1:8080/v1/thumbnail?width=256&quality=80"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A file that is not an image or a PDF, or a PDF that cannot be read, returns `400`. An upload larger than `WEBTHUMB_MAX_UPLOAD_MB` (default 32) returns `413`. The endpoint reads only the upload. It does not accept a filesystem path.
|
|
59
|
+
|
|
60
|
+
## Watch
|
|
61
|
+
|
|
62
|
+
`webpthumb watch` thumbnails files already in the directory, then stays running until SIGINT or SIGTERM. New and updated files are picked up with watchdog. The watcher waits until a file's size stops changing (about half a second) so a copy in progress is not read early.
|
|
63
|
+
|
|
64
|
+
`webpthumb watch --background` and `webpthumb start` do that work in the background and return. `webpthumb status` prints whether that watcher is running. `webpthumb stop` sends SIGTERM and waits for it to exit. A second start is refused while the first is still running. The pid, the directories, and the log live under `WEBTHUMB_RUN_DIR` (`$XDG_STATE_HOME/webpthumb`, or `~/.local/state/webpthumb`). A foreground `watch` is not tracked. Keep `watch` in the foreground when it is the main process of a container.
|
|
65
|
+
|
|
66
|
+
Watched extensions: `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.tif`, `.tiff`, `.webp`, `.ico`, `.heic`, `.heics`, `.heif`, `.heifs`, `.hif`, `.avif`, `.avifs`, `.pdf`. Hidden files and `*.part` files are ignored. The output directory must not sit inside the input directory. With `-r`, the output tree mirrors the input tree.
|
|
67
|
+
|
|
68
|
+
A thumbnail is rewritten when the source is newer than the WebP, or when `--overwrite` is set.
|
|
69
|
+
|
|
70
|
+
`webpthumb serve` starts the same watcher in the background when both `WEBTHUMB_WATCH` and `WEBTHUMB_OUTPUT` are set.
|
|
71
|
+
|
|
72
|
+
## Environment
|
|
73
|
+
|
|
74
|
+
Used when the matching flag or query parameter is omitted.
|
|
75
|
+
|
|
76
|
+
| Variable | Default | Used by |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `WEBTHUMB_WIDTH` | `256` | convert, serve, watch |
|
|
79
|
+
| `WEBTHUMB_HEIGHT` | unset | convert, serve, watch |
|
|
80
|
+
| `WEBTHUMB_QUALITY` | `80` | convert, serve, watch |
|
|
81
|
+
| `WEBTHUMB_PAGES` | `1` | convert, watch |
|
|
82
|
+
| `WEBTHUMB_CROP_TOP` | unset | convert, serve, watch |
|
|
83
|
+
| `WEBTHUMB_OVERWRITE` | off | convert, watch (`1`, `true`, `yes`, `on`) |
|
|
84
|
+
| `WEBTHUMB_RECURSIVE` | off | convert, watch |
|
|
85
|
+
| `WEBTHUMB_JOBS` | CPU count | convert |
|
|
86
|
+
| `WEBTHUMB_OUTPUT` | `./thumbnails` for convert | convert, watch, serve |
|
|
87
|
+
| `WEBTHUMB_WATCH` | unset | serve, with `WEBTHUMB_OUTPUT` |
|
|
88
|
+
| `WEBTHUMB_HOST` | `127.0.0.1` | serve (the image sets `0.0.0.0`) |
|
|
89
|
+
| `WEBTHUMB_PORT` | `8080` | serve |
|
|
90
|
+
| `WEBTHUMB_MAX_UPLOAD_MB` | `32` | serve |
|
|
91
|
+
| `WEBTHUMB_RUN_DIR` | `~/.local/state/webpthumb` | start, stop, status |
|
|
92
|
+
|
|
93
|
+
## Docker
|
|
94
|
+
|
|
95
|
+
The image is `python:3.12-slim` plus this package. PyMuPDF carries its own MuPDF build, so poppler and ImageMagick are not installed.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
docker build -t webpthumb .
|
|
99
|
+
docker run --rm -p 8080:8080 webpthumb
|
|
100
|
+
|
|
101
|
+
# API and watcher together
|
|
102
|
+
docker compose up --build
|
|
103
|
+
|
|
104
|
+
# watcher only
|
|
105
|
+
docker run --rm -v "$PWD/in:/data/in" -v "$PWD/out:/data/out" \
|
|
106
|
+
webpthumb watch /data/in -o /data/out -r
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`docker compose up` publishes port 8080 and mounts `./in` and `./out`. Drop a PNG, HEIC, or PDF into `./in` and the thumbnail appears in `./out`.
|
|
110
|
+
|
|
111
|
+
## Tests
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pytest
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Library
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from webpthumb import ThumbOptions, make_thumbnail, write_thumbnail
|
|
121
|
+
|
|
122
|
+
webp = make_thumbnail(open("doc.pdf", "rb").read(), "doc.pdf", ThumbOptions(width=256))
|
|
123
|
+
paths = write_thumbnail("photo.png", "thumbnails", ThumbOptions(width=256, quality=80))
|
|
124
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "webpthumb"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "WebP thumbnails for images and PDFs, from the CLI, HTTP, or a folder watcher"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["webp", "thumbnail", "pdf", "image", "heif", "heic", "watchdog"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"pdfthumb>=0.1.2",
|
|
15
|
+
"image-to-webp[heif]>=0.1.0",
|
|
16
|
+
"watchdog>=4",
|
|
17
|
+
"fastapi>=0.110",
|
|
18
|
+
"uvicorn>=0.27",
|
|
19
|
+
"python-multipart>=0.0.9",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = ["pytest>=7", "httpx>=0.27"]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
webpthumb = "webpthumb.cli:main"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
33
|
+
pythonpath = ["src", "tests"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""WebP thumbnails for images and PDFs."""
|
|
2
|
+
|
|
3
|
+
from .core import ThumbError, ThumbResult, make_thumbnail, write_thumbnail
|
|
4
|
+
from .options import ThumbOptions
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ThumbError",
|
|
10
|
+
"ThumbOptions",
|
|
11
|
+
"ThumbResult",
|
|
12
|
+
"make_thumbnail",
|
|
13
|
+
"write_thumbnail",
|
|
14
|
+
"__version__",
|
|
15
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""HTTP endpoint that returns a WebP thumbnail for an uploaded image or PDF."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI, HTTPException, UploadFile
|
|
10
|
+
from fastapi.responses import Response
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .core import ThumbError, make_thumbnail
|
|
14
|
+
from .options import ThumbOptions
|
|
15
|
+
|
|
16
|
+
_SAFE = re.compile(r"[^A-Za-z0-9._-]+")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _env_int(name: str, default: int) -> int:
|
|
20
|
+
raw = os.environ.get(name)
|
|
21
|
+
if not raw:
|
|
22
|
+
return default
|
|
23
|
+
try:
|
|
24
|
+
return int(raw)
|
|
25
|
+
except ValueError:
|
|
26
|
+
return default
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _env_optional_int(name: str) -> int | None:
|
|
30
|
+
raw = os.environ.get(name)
|
|
31
|
+
if not raw:
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
return int(raw)
|
|
35
|
+
except ValueError:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _env_optional_float(name: str) -> float | None:
|
|
40
|
+
raw = os.environ.get(name)
|
|
41
|
+
if not raw:
|
|
42
|
+
return None
|
|
43
|
+
try:
|
|
44
|
+
return float(raw)
|
|
45
|
+
except ValueError:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def upload_limit_bytes() -> int:
|
|
50
|
+
megabytes = _env_int("WEBTHUMB_MAX_UPLOAD_MB", 32)
|
|
51
|
+
if megabytes < 1:
|
|
52
|
+
megabytes = 32
|
|
53
|
+
return megabytes * 1024 * 1024
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def safe_stem(filename: str | None) -> str:
|
|
57
|
+
stem = Path(filename or "thumbnail").stem
|
|
58
|
+
cleaned = _SAFE.sub("_", stem).strip("._")
|
|
59
|
+
return (cleaned or "thumbnail")[:120]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_app(max_upload_bytes: int | None = None) -> FastAPI:
|
|
63
|
+
"""Build the API. Query defaults fall back to ``WEBTHUMB_*`` environment variables."""
|
|
64
|
+
limit = upload_limit_bytes() if max_upload_bytes is None else max_upload_bytes
|
|
65
|
+
app = FastAPI(title="webpthumb", version=__version__)
|
|
66
|
+
app.state.max_upload_bytes = limit
|
|
67
|
+
|
|
68
|
+
@app.get("/health")
|
|
69
|
+
def health() -> dict[str, str]:
|
|
70
|
+
return {"status": "ok"}
|
|
71
|
+
|
|
72
|
+
@app.post("/v1/thumbnail")
|
|
73
|
+
async def thumbnail(
|
|
74
|
+
file: UploadFile,
|
|
75
|
+
width: int | None = None,
|
|
76
|
+
height: int | None = None,
|
|
77
|
+
quality: int | None = None,
|
|
78
|
+
page: int = 1,
|
|
79
|
+
crop_top: float | None = None,
|
|
80
|
+
) -> Response:
|
|
81
|
+
data = await _read_limited(file, app.state.max_upload_bytes)
|
|
82
|
+
if not data:
|
|
83
|
+
raise HTTPException(status_code=400, detail="empty upload")
|
|
84
|
+
resolved_width = width if width is not None else _env_int("WEBTHUMB_WIDTH", 256)
|
|
85
|
+
resolved_height = height if height is not None else _env_optional_int("WEBTHUMB_HEIGHT")
|
|
86
|
+
resolved_quality = quality if quality is not None else _env_int("WEBTHUMB_QUALITY", 80)
|
|
87
|
+
resolved_crop = crop_top if crop_top is not None else _env_optional_float("WEBTHUMB_CROP_TOP")
|
|
88
|
+
filename = file.filename or "thumbnail"
|
|
89
|
+
try:
|
|
90
|
+
options = ThumbOptions(
|
|
91
|
+
width=resolved_width,
|
|
92
|
+
height=resolved_height,
|
|
93
|
+
quality=resolved_quality,
|
|
94
|
+
crop_top=resolved_crop,
|
|
95
|
+
)
|
|
96
|
+
webp = make_thumbnail(data, filename, options, page=page)
|
|
97
|
+
except ThumbError as exc:
|
|
98
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
99
|
+
except ValueError as exc:
|
|
100
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
101
|
+
stem = safe_stem(filename)
|
|
102
|
+
return Response(
|
|
103
|
+
content=webp,
|
|
104
|
+
media_type="image/webp",
|
|
105
|
+
headers={"Content-Disposition": f'inline; filename="{stem}.webp"'},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return app
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def _read_limited(file: UploadFile, limit: int) -> bytes:
|
|
112
|
+
chunks: list[bytes] = []
|
|
113
|
+
total = 0
|
|
114
|
+
while True:
|
|
115
|
+
block = await file.read(1024 * 1024)
|
|
116
|
+
if not block:
|
|
117
|
+
break
|
|
118
|
+
total += len(block)
|
|
119
|
+
if total > limit:
|
|
120
|
+
raise HTTPException(status_code=413, detail="upload exceeds limit")
|
|
121
|
+
chunks.append(block)
|
|
122
|
+
return b"".join(chunks)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
app = create_app()
|