81 lines
6.2 KiB
Markdown
81 lines
6.2 KiB
Markdown
# 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](https://sharp.pixelplumbing.com/) 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](docs/INSTALL.md)** — start here. Local dev, Docker, `docker compose`, the full env-var table, CI, health checks, upgrades.
|
||
- **[Unraid install](docs/UNRAID.md)** — step-by-step Docker GUI walkthrough (template fields, PUID/PGID/TZ, ports).
|
||
- **[Development & technical notes](docs/INSTRUCTIONS.md)** — architecture, the live-preview design, code standards, troubleshooting.
|
||
- **[Roadmap](docs/ROADMAP.md)** — 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 OS `prefers-color-scheme`. The point is practical: toggle the background to check PNG transparency against black and white.
|
||
- **Stores nothing** — `multer` 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 `tsc` → `node: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):**
|
||
|
||
```bash
|
||
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+:
|
||
|
||
```bash
|
||
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](docs/INSTALL.md)**.
|
||
|
||
## Known rough edges
|
||
|
||
Documented rather than hidden — see [docs/INSTRUCTIONS.md](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.*
|