6.2 KiB
PNGer — Modern PNG Editor & Resizer
A sleek, self-hosted image resizer with live preview, drag & drop, smart presets, keyboard shortcuts, and dark/light theming. Drop an image in, watch the before/after and the file-size delta update as you drag the sliders, hit Enter, get the file. Nothing is stored on the server — uploads are processed in memory and streamed straight back as a download.
Svelte + Vite on the front, Express + Sharp on the back, shipped as one Docker container that serves both. Built for Unraid.
Despite the name it is not PNG-only: it reads anything Sharp can decode and writes PNG, WebP or JPEG.
Docs
- Installation & configuration guide — start here. Local dev, Docker,
docker compose, the full env-var table, CI, health checks, upgrades. - Unraid install — step-by-step Docker GUI walkthrough (template fields, PUID/PGID/TZ, ports).
- Development & technical notes — architecture, the live-preview design, code standards, troubleshooting.
- Roadmap — what shipped in Sprints 0–1 and what is queued for Sprints 2–4.
What it does
- Resizes and crops — set a width, a height, or both. Resize only (
fit: inside) preserves aspect ratio; Crop to fit box (fit: cover) fills the box and crops the overflow from one of nine anchor positions (center, the four edges, the four corners). - Converts and compresses — output as PNG (compression level 9), WebP or JPEG, with a 1–100 quality control.
- Previews client-side before it touches the server — the browser re-renders the image on a
<canvas>300 ms after you stop typing, so you get a side-by-side original/result comparison plus an estimated file-size delta ("↓ 412.3 KB saved (68.1%)") with no round-trip. Only the final download hits the API. - Eight smart presets — Web Thumbnail (300×300 WebP), Social Media / Open Graph (1200×630), Profile Picture (400×400), Email Friendly (600 wide JPEG q70), HD Quality (1920 wide), Retina @2x (doubles whatever you have typed), Icon Small (64×64) and Icon Large (256×256). One click sets width, height, quality, format and fit.
- Gets images in three ways — drag & drop onto the drop zone, click to browse, or paste from the clipboard with
Ctrl+V/Cmd+V. - Themes light or dark — gold-accented design system driven by CSS custom properties, remembered in
localStorage, defaulting to your OSprefers-color-scheme. The point is practical: toggle the background to check PNG transparency against black and white. - Stores nothing —
multerkeeps the upload in memory, Sharp transforms it, Express sends the buffer back withContent-Disposition: attachment. No database, no disk writes, no cleanup job.
Keyboard shortcuts
| Key | Action |
|---|---|
Ctrl+V / Cmd+V |
Paste an image from the clipboard |
Enter |
Transform & download (when no input is focused) |
Ctrl+Enter / Cmd+Enter |
Transform & download (works from anywhere) |
? |
Toggle the shortcuts dialog |
Esc |
Close the shortcuts dialog |
Shape
- Stack — frontend: Svelte 4 + Vite 5 + TypeScript, no UI framework, no router, one
App.svelteplus fourlib/modules (api,preview,presets,theme). Backend: Node 20 + Express 4 + TypeScript,sharpfor the pixels,multer(memory storage) for the upload,cors. - API — one endpoint.
POST /api/transform,multipart/form-data, fieldfile, with optionalwidth,height,quality(default 80),format(png|webp|jpeg, defaultpng),fit(defaultinside) andposition(defaultcenter). Responds with the image bytes as an attachment, or400if no file /500if Sharp throws. Resizes usewithoutEnlargement: true, so output is never larger than the source. - Data — none. Stateless; no volumes required.
- Serving — in production the compiled frontend is copied to
dist/publicand served by the same Express process that answers/api, with anapp.get("*")fallback toindex.htmlfor SPA routing. One port, one origin, no CORS in play. - Hosting — three-stage Docker build (frontend build → backend
tsc→node:20-alpineruntime withapk upgrade,su-exec+shadow).docker-entrypoint.shremaps thenodeuser toPUID/PGID(Unraid defaults 99/100), chowns/app, then drops privileges. Listens on3000,HEALTHCHECKevery 30 s,EXPOSE 3000. CI builds on every push tomainand pushesregistry.alwisp.com/jason/pnger:latest.
Quick start
Docker (single container):
git clone https://git.alwisp.com/jason/pnger.git
cd pnger
docker compose up -d --build
Then open http://localhost:8080 (host port is HOST_PORT, default 8080 → container 3000).
Local development — two terminals, Node 20+:
cd backend && npm install && npm run dev # tsc watch on :3000
cd frontend && npm install && npm run dev # Vite on :5173
Open http://localhost:5173. In dev the frontend calls http://localhost:3000/api directly (import.meta.env.DEV branch in lib/api.ts); in a production build it calls the same-origin /api.
Full details, env vars and the Unraid path: docs/INSTALL.md.
Known rough edges
Documented rather than hidden — see docs/INSTRUCTIONS.md for the detail:
- The preview is an approximation. It is a Canvas re-encode, not Sharp. Sizes are estimated from the data URL, PNG quality has no effect in Canvas (it is always lossless there) but does affect the real output, and the preview will happily upscale where the server will not.
MAX_FILE_SIZEis not enforced. It is set in theDockerfile, compose file and Unraid docs, but the live upload middleware has nolimits, so the value currently does nothing.backend/src/server.jsis not the server. It is an older standalone implementation (/api/health,/api/process,/api/metadata, helmet, PNG-only filter, size limits) that nothing compiles or runs. The live entrypoint isbackend/src/index.ts.
License: MIT · Repository: https://git.alwisp.com/jason/pnger
MPM — Born to Innovate. Built to Last.