Image tools
A Python script the Bot runs on the computer, and a result card with pictures.
app.json
{
"format": "deepbot.app/v1",
"id": "image-tools",
"name": "Image tools",
"version": "1.0.0",
"tier": "free",
"vendor": "DeepBot examples",
"summary": "Resize and convert a folder of pictures on your computer, and see the results in the chat.",
"description": "Your Bot runs the app's Python script on your computer to resize, convert (JPG, PNG, WebP) and compress pictures, then shows the results side by side in the chat with their sizes.",
"icon": "ui/icon.svg",
"permissions": {
"files": ["images"],
"computer": true
},
"skill": "skill/SKILL.md",
"cards": [
{
"name": "showResizedImages",
"title": "Pictures",
"entry": "ui/card.html",
"height": 260,
"description": "Show the pictures in a folder under images/ as a grid with their sizes, after resizing. Pass `dir` (e.g. images/out/holiday) and optionally `before` (the folder they came from, to show how much smaller they got).",
"parameters": {
"type": "object",
"properties": {
"dir": { "type": "string" },
"before": { "type": "string" }
},
"required": ["dir"]
}
}
]
}
skill/SKILL.md
---
name: image-tools
description: "Use when the person wants pictures resized, made smaller, compressed or converted (JPG, PNG, WebP), one or a whole folder: runs the Image tools script and shows the results."
---
# Image tools
The app's script is `{{APP_DIR}}/scripts/resize.py`. It needs Python 3 with Pillow; if
`import PIL` fails, install it once with `python -m pip install --user pillow`.
## Where the pictures go
The app works in the workspace folder `images/`: copy or download the person's pictures into
`images/in/<name>/` first (ask where they are if they did not say), and write the results to
`images/out/<name>/`.
## Running it
```
python {{APP_DIR}}/scripts/resize.py images/in/<name> images/out/<name> --width 1600 --format webp --quality 82
```
- `--width` the longest side in pixels (keeps the proportions; never makes a picture bigger)
- `--format` `jpg`, `png`, `webp` or `keep`
- `--quality` 1-100 for jpg and webp (82 is a good default)
It prints one JSON line per picture and a summary line at the end:
`{"done": 12, "before": 48211033, "after": 6120044}`.
## Showing the result
Call `showResizedImages` with `dir` set to the output folder and `before` to the input folder. Then say in
one sentence how much smaller it got (from the summary line).
ui/card.html
<!doctype html>
<html lang="en">
<head><meta charset="utf-8">
<style>
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; margin-top: 10px; }
.grid figure { margin: 0; }
.grid img { width: 100%; aspect-ratio: 4/3; object-fit: cover; border-radius: 8px; background: var(--muted); display: block; }
.grid figcaption { font-size: 11px; color: var(--muted-foreground); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.saved { font-size: 22px; font-weight: 700; }
</style>
</head>
<body>
<div class="db-card" id="root"><span class="db-muted">Reading the folder…</span></div>
<script src="card.js"></script>
</body>
</html>
ui/card.js
/*
* THE RESULT IN THE CHAT: the pictures of a folder under images/, with their sizes. Pictures are
* shown with deepbot.files.url(path), an address the card may put in <img src>.
*/
const root = document.getElementById("root");
const IMG = /\.(jpe?g|png|webp|gif|bmp)$/i;
const kb = (n) => (n > 1e6 ? `${(n / 1e6).toFixed(1)} MB` : `${Math.round(n / 1e3)} KB`);
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
async function sizeOf(dir) {
if (!dir) return 0;
const { items } = await deepbot.files.list(dir);
return items.filter((i) => !i.dir && IMG.test(i.name)).reduce((n, i) => n + i.size, 0);
}
async function show() {
const dir = String(deepbot.args.dir || "");
try {
const { items } = await deepbot.files.list(dir);
const pics = items.filter((i) => !i.dir && IMG.test(i.name));
if (!pics.length) throw new Error(`No pictures in ${dir}.`);
const after = pics.reduce((n, i) => n + i.size, 0);
const before = await sizeOf(deepbot.args.before);
root.innerHTML = `
<div class="db-row" style="justify-content:space-between">
<div><div class="db-h2">${pics.length} pictures</div><div class="db-muted">${esc(dir)}</div></div>
<div style="text-align:right">${before ? `<div class="saved">−${Math.round((1 - after / before) * 100)}%</div><div class="db-muted">${kb(before)} → ${kb(after)}</div>` : `<div class="db-muted">${kb(after)}</div>`}</div>
</div>
<div class="grid">${pics
.slice(0, 24)
.map((p) => `<figure><img src="${deepbot.files.url(p.path)}" alt="" loading="lazy"><figcaption>${esc(p.name)} · ${kb(p.size)}</figcaption></figure>`)
.join("")}</div>`;
deepbot.done(`${pics.length} pictures shown.`);
} catch (error) {
root.innerHTML = `<span class="db-muted">${esc(error.message)}</span>`;
deepbot.fail(error.message);
}
}
deepbot.ready.then(show);
ui/icon.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>Image tools</title><rect width="64" height="64" rx="16" fill="#10B981"/><rect x="13" y="16" width="38" height="30" rx="5" fill="#fff"/><circle cx="24" cy="26" r="4" fill="#FFD43B"/><path d="m16 43 11-11 8 8 5-5 8 8" fill="none" stroke="#14161C" stroke-width="3" stroke-linejoin="round"/></svg>
scripts/resize.py
"""Resize and convert every picture in a folder. One JSON line per picture, a summary line at the end.
python resize.py <in_dir> <out_dir> [--width 1600] [--format keep|jpg|png|webp] [--quality 82]
"""
import argparse
import json
import sys
from pathlib import Path
try:
from PIL import Image, ImageOps
except ImportError:
print(json.dumps({"error": "Pillow is missing: python -m pip install --user pillow"}))
sys.exit(2)
EXT = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
SAVE = {"jpg": ("JPEG", ".jpg"), "png": ("PNG", ".png"), "webp": ("WEBP", ".webp")}
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("src")
p.add_argument("dst")
p.add_argument("--width", type=int, default=1600)
p.add_argument("--format", default="keep", choices=["keep", "jpg", "png", "webp"])
p.add_argument("--quality", type=int, default=82)
a = p.parse_args()
src, dst = Path(a.src), Path(a.dst)
if not src.is_dir():
print(json.dumps({"error": f"No folder {src}"}))
return 1
dst.mkdir(parents=True, exist_ok=True)
done = before = after = 0
for f in sorted(src.iterdir()):
if f.suffix.lower() not in EXT:
continue
try:
img = ImageOps.exif_transpose(Image.open(f))
img.thumbnail((a.width, a.width))
fmt, ext = SAVE.get(a.format) or SAVE.get(f.suffix.lower().lstrip(".").replace("jpeg", "jpg"), ("PNG", ".png"))
if fmt == "JPEG" and img.mode not in ("RGB", "L"):
img = img.convert("RGB")
out = dst / (f.stem + ext)
kw = {"quality": a.quality, "optimize": True} if fmt in ("JPEG", "WEBP") else {"optimize": True}
img.save(out, fmt, **kw)
b, s = f.stat().st_size, out.stat().st_size
done, before, after = done + 1, before + b, after + s
print(json.dumps({"file": out.name, "width": img.width, "height": img.height, "before": b, "after": s}), flush=True)
except Exception as e: # one bad file does not stop the rest
print(json.dumps({"file": f.name, "error": str(e)}), flush=True)
print(json.dumps({"done": done, "before": before, "after": after}))
return 0
if __name__ == "__main__":
sys.exit(main())
listing.md
---
categories: [design]
tags: [images, resize, convert, webp, example]
screenshots:
- Resized pictures in the chat
---
An example app from the DeepBot docs that runs a **script on your computer**. Ask your Bot to
“make these photos smaller for the web” and it resizes and converts them (JPG, PNG, WebP) with the
app's Python script, then shows the results with how much space they saved.
Needs Python 3 with Pillow on the computer (the Bot installs Pillow if it is missing).
## Changelog
### 1.0.0
- First version.