Local‑first batch image cropper and converter for a fixed 800×480 output. Load a folder of images, preview a fixed‑aspect crop, nudge the vertical position, and convert everything in one go. The backend streams a ZIP of 16‑bit RGB565 BMPs with Floyd–Steinberg dithering — ideal for small TFT devices (e.g., PhotoPainter).
Vibe coded with ❤️ by Omer van Kloeten for local, private, and efficient image prep.
Original image:
Converted image (800x480, dithered 6-color BMP):
Displayed on the PhotoPainter (A):
All images used in the project are public domain.
- Local‑only workflow: images never leave your machine
- Drag‑and‑drop files or select an entire folder
- Fixed 800×480 aspect preview; drag vertically to position the crop band
- Smart initial crop (placeholder today, face‑detection PRD ready)
- Batch convert to BMP (RGB565) with dithering; streamed ZIP download
- Live progress via Server‑Sent Events (SSE)
- Persistent selection and crop positions using IndexedDB (restored on reload)
- Polished UI with shadcn/ui, toasts, and keyboard‑friendly interactions
- Frontend: Vite + React (TypeScript), React Router v6, Tailwind CSS, shadcn/ui
- State/UX: TanStack Query provider, shadcn Toaster + Sonner
- Backend: Node.js 22 + Fastify (multipart uploads, health, routes), Pino logs
- Image processing: sharp (EXIF rotate, sRGB, crop/resize) + ImageMagick (dither, posterize, BMP565)
- Packaging: archiver streams ZIP responses
-
Frontend
src/pages/Index.tsxorchestrates the flow: upload → preview → convertsrc/components/ImageUpload.tsxhandles drag‑and‑drop and folder selectionsrc/components/ImageGallery.tsxloads images, requests an initial crop suggestion, tracks progress, and triggers conversionsrc/components/ImageCropPreview.tsxrenders the image with a fixed‑aspect crop band you can drag verticallysrc/lib/storage.tspersists images +ycrop using IndexedDBsrc/lib/api.tswraps calls to the backend
-
Backend
GET /api/health— service statusPOST /api/suggest-crop— returns initial vertical cropyand natural dimensions- Current implementation returns
y = 0; see PRD for face detection
- Current implementation returns
POST /api/convert— accepts a manifest + files and streams a ZIP of converted BMPsGET /api/progress/:jobId— Server‑Sent Events for live progress during conversion- See
server/src/lib/image.tsfor crop math and the sharp → ImageMagick pipeline
-
Processing pipeline (per image)
sharp: rotate (EXIF), convert to sRGB- Compute cropHeight = naturalWidth / (800/480), clamp
y, extract band, resize to 800×480 magick:-dither FloydSteinberg -posterize 2 -depth 5,6,5 bmp3:-- Stream each BMP into a ZIP via
archiver
- Node.js 22 (LTS)
- ImageMagick 7+ CLI (
magick) available in PATH - libvips 8.14+ (required by sharp)
macOS (Homebrew):
brew install vips imagemagickLinux (Debian/Ubuntu):
sudo apt-get update
sudo apt-get install -y libvips imagemagickWindows:
- Install ImageMagick (ensuring
magick.exeis on PATH) - sharp bundles libvips; no separate install usually required
Verify tools:
vips --version
magick -version
node -vHEIC/HEIF decoding requires libvips built with libheif.
macOS (Homebrew):
brew install libheifDebian/Ubuntu:
sudo apt-get update
sudo apt-get install -y libheif1 libheif-devFedora:
sudo dnf install -y libheif libheif-develArch Linux:
sudo pacman -S libheifAlpine:
sudo apk add libheif-devIf you add libheif after installing dependencies, reinstall sharp so it picks up support:
rm -rf node_modules
npm install- Install dependencies
npm install- Start the backend (terminal A)
npm run server:dev- Start the frontend (terminal B)
npm run dev- Open the app at
http://localhost:8080
Notes
- The Vite dev server proxies
/api/*tohttp://127.0.0.1:8787(seevite.config.ts), so no CORS hassles in development. - The backend binds to
127.0.0.1:8787with sensible multipart limits.
- Drag‑and‑drop images or click to select images/folders (PNG/JPG/JPEG/GIF/BMP/WebP/HEIC/HEIF)
- For each image, adjust the crop band’s vertical position
- Click “Convert All” to start processing
- Watch progress; a
converted.zipfile downloads automatically on completion
Output naming: each image becomes <original_base>_800x480.bmp inside the ZIP.
Limits (defaults)
- Up to 200 files per request
- Max 30 MB per file
Base URL (dev): http://127.0.0.1:8787/api
curl -sS http://127.0.0.1:8787/api/healthMultipart form with image file; returns { y, naturalWidth, naturalHeight }.
curl -sS -X POST http://127.0.0.1:8787/api/suggest-crop \
-F image=@/path/to/image.jpgSend a JSON manifest plus repeated files entries. Optional jobId correlates with SSE progress.
curl -sS -X POST http://127.0.0.1:8787/api/convert \
-F manifest='{"jobId":"YOUR_JOB_ID","images":[{"fileName":"a.jpg","y":23},{"fileName":"b.png","y":51}]}' \
-F files=@/path/to/a.jpg \
-F files=@/path/to/b.png \
-o converted.zipcurl -N http://127.0.0.1:8787/api/progress/YOUR_JOB_ID- Dev server: Vite on port 8080; proxy for
/api→http://127.0.0.1:8787 - Path alias:
@→./src - Backend environment: by default CORS is disabled; development uses the proxy. If you plan to call the backend directly from a different origin, you’ll need to enable and configure CORS appropriately in
server/src/index.ts. - Concurrency: conversion uses a small in‑process limiter (currently 2). Adjust in
server/src/routes/convert.tsif needed.
- Frontend
npm run build
# Preview the static build
npm run preview- Backend
npm run server:build
npm run server:startYou can serve the frontend’s dist/ with any static web server (or a CDN) and run the backend separately. In production deployments across different origins, configure CORS on the backend and point the frontend to it (adjusting your proxy or API base URL as needed).
src/— React apppages/(Index.tsx,NotFound.tsx)components/(feature components +ui/primitives)lib/(api.ts,storage.ts,utils.ts)main.tsx,App.tsx
server/— Fastify backendsrc/index.ts(app + route registration)src/routes/(suggest.ts,convert.ts,progress.ts)src/lib/(image.tsfor crop/convert,progress.tsfor SSE jobs)
docs/prd/— PRDs for backend and face detection
- Face detection–based crop suggestions (
docs/prd/face-detection.md) - Horizontal crop control: add
xcoordinate and horizontal sliding of the crop window (UI + server) - Keyboard nudging, zoom, and accessibility improvements
-
magick: command not found- Install ImageMagick 7+ and ensure
magickis on PATH (macOS:brew install imagemagick).
- Install ImageMagick 7+ and ensure
-
sharp install/runtime errors
- Ensure Node 22 is used. On Linux/macOS, install
libvips(see prerequisites). Then reinstall:rm -rf node_modules && npm install. - For HEIC/HEIF: sharp relies on libvips built with libheif. Homebrew
vipsincludes libheif by default; on Linux, installlibheifalongsidelibvips.
- Ensure Node 22 is used. On Linux/macOS, install
-
“CORS” errors in production
- In dev, the Vite proxy avoids CORS. For non‑proxy deployments, enable/configure CORS in the backend and ensure your frontend points to the correct API origin.
This project is licensed under the Apache License 2.0. See LICENSE for the full text and NOTICE for attribution requirements.
If you use or redistribute this project, please include attribution to “DitherWorks for PhotoPainter” as described in NOTICE. We’d also love to hear about your use—feel free to open an issue or PR in this repository to share what you built.



