Files
pnger/README.md
T

6.2 KiB
Raw Blame History

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 01 and what is queued for Sprints 24.

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 1100 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 OS prefers-color-scheme. The point is practical: toggle the background to check PNG transparency against black and white.
  • Stores nothingmulter keeps the upload in memory, Sharp transforms it, Express sends the buffer back with Content-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.svelte plus four lib/ modules (api, preview, presets, theme). Backend: Node 20 + Express 4 + TypeScript, sharp for the pixels, multer (memory storage) for the upload, cors.
  • API — one endpoint. POST /api/transform, multipart/form-data, field file, with optional width, height, quality (default 80), format (png | webp | jpeg, default png), fit (default inside) and position (default center). Responds with the image bytes as an attachment, or 400 if no file / 500 if Sharp throws. Resizes use withoutEnlargement: 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/public and served by the same Express process that answers /api, with an app.get("*") fallback to index.html for SPA routing. One port, one origin, no CORS in play.
  • Hosting — three-stage Docker build (frontend build → backend tscnode:20-alpine runtime with apk upgrade, su-exec + shadow). docker-entrypoint.sh remaps the node user to PUID/PGID (Unraid defaults 99/100), chowns /app, then drops privileges. Listens on 3000, HEALTHCHECK every 30 s, EXPOSE 3000. CI builds on every push to main and pushes registry.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_SIZE is not enforced. It is set in the Dockerfile, compose file and Unraid docs, but the live upload middleware has no limits, so the value currently does nothing.
  • backend/src/server.js is 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 is backend/src/index.ts.

License: MIT · Repository: https://git.alwisp.com/jason/pnger


MPM — Born to Innovate. Built to Last.