Files
pnger/docs/INSTRUCTIONS.md
T

7.9 KiB

PNGer Development & Technical Instructions

Project Overview

PNGer is a single-container web application for PNG editing and resizing, designed for deployment on Unraid with Gitea version control. It features a modern Svelte frontend and a high-performance Node.js/Sharp backend.

Technical Architecture

Architecture Overview

PNGer uses a multi-layered architecture designed for responsiveness and efficiency:

graph TD
    A[User Browser] --> B[Svelte Frontend]
    B --> C["Canvas API (live preview, client-side)"]
    B --> D["Express API (POST /api/transform)"]
    D --> E[Sharp Image Library]
    E --> F[Response buffer streamed back as a download]

Nothing is persisted at any point: the upload lives in memory for the duration of the request and the result is streamed straight back.

Backend (Express + Sharp)

The backend is built with Node.js and TypeScript, using Express for the API and Sharp for high-performance image processing.

Endpoints — there is exactly one:

  • POST /api/transformmultipart/form-data, field file, plus optional width, height, quality (default 80), format (png | webp | jpeg, default png), fit (default inside) and position (default center). Returns the image bytes with Content-Disposition: attachment; 400 if no file, 500 if Sharp throws.

There is no GET /api/health. Earlier revisions of this document claimed one. The router defines only POST /transform, so a GET to /api/health falls through to the SPA catch-all and returns index.html with a 200 — which means a monitor pointed at it will always report healthy. The container's own HEALTHCHECK correctly probes / instead.

Key Dependencies:

  • sharp: High-performance image processing (handles resizing, cropping, and format conversion). Resizes use withoutEnlargement: true, so output never exceeds the source dimensions.
  • multer: Middleware for handling multipart/form-data, used for file uploads. Configured with memoryStorage() and no limits — see the dead-code note below.
  • express: Web framework for the API; also serves the built frontend from dist/public with an app.get("*") SPA fallback.
  • cors: Applied wide open (app.use(cors())) so the Vite dev server on :5173 can reach the API on :3000. In production both are the same origin.

Dead code you will trip over

  • backend/src/server.js is not the server. The entrypoint is src/index.tsdist/index.js. server.js is an older standalone CommonJS implementation with /api/health, /api/process and /api/metadata, helmet, a PNG-only file filter and a real MAX_FILE_SIZE limit. tsconfig.json has no allowJs, so tsc never compiles it and it never reaches dist/; it also requires helmet and dotenv, which are not in package.json. It is the source of most of the features these docs used to describe. Either port the good parts (size limit, MIME filter, real health endpoint) into routes/image.ts or delete it.
  • frontend/src/main.js and frontend/src/main.ts both exist. index.html loads main.js, and only main.js imports ./app.css — "fixing" the script tag to point at main.ts would silently drop every style in the app.

Frontend (Svelte + Vite)

The frontend is a reactive Svelte application that prioritizes real-time feedback and UX.

Core Components & Modules:

  • App.svelte: Main application container managing state and UI.
  • lib/api.ts: API client for backend communication.
  • lib/preview.ts: Client-side preview logic using the Canvas API for instant feedback.
  • lib/theme.ts: Dark/light theme management with persistent storage.
  • lib/presets.ts: Configuration for smart image presets.

Design System (Dark/Light Modes)

The UI uses a modern design system with CSS custom properties for theming:

  • Light Mode: Clean white background with dark gold (#b8860b) accents.
  • Dark Mode: Deep black (#0a0a0a) background with light gold (#daa520) accents.
  • Transparencies: Dark/Light toggling allows users to inspect PNG transparency against different backgrounds.

Implementation Details

Live Preview System

Live preview is implemented using a client-side Canvas-based approach to provide instant feedback (< 100ms) without server round-trips.

How it works:

  1. User uploads an image.
  2. The browser loads the image into an HTML5 Canvas.
  3. On any parameter change (width, quality, etc.), a debounced (300ms) update applies transformations to the canvas.
  4. The canvas content is displayed side-by-side with the original for comparison.
  5. File sizes are estimated from the Canvas data URL to provide immediate feedback on optimization savings.

Fidelity caveats — the preview is an approximation, not the output:

  • The preview is encoded by the browser's Canvas encoder; the download is encoded by Sharp. Byte counts will differ.
  • Canvas ignores the quality argument for PNG (it is always lossless), so dragging quality changes nothing in a PNG preview while it does change the real output.
  • The preview's calculateDimensions() will happily scale an image up; the server sets withoutEnlargement: true and will not. The "Retina @2x" preset is exactly this case — the preview grows, the downloaded file does not.

Docker Strategy & Fixes

PNGer uses a multi-stage Docker build to minimize image size and maximize security.

Optimization Fixes applied:

  • Dependency Installation: Uses npm install instead of npm ci to handle missing lockfiles gracefully while maintaining stability via caret ranges in package.json.
  • Security: Runs as a non-root node user in the final Alpine-based image.
  • Multer Upgrade: Upgraded to v2.1.0 for improved security and performance.

Local Development Setup

Prerequisites

  • Node.js 20+
  • npm or pnpm
  • Docker (optional)

Setup Steps

# Clone repository
git clone https://git.alwisp.com/jason/pnger.git
cd pnger

# Setup backend
cd backend
npm install
npm run dev

# Setup frontend (separate terminal)
cd frontend
npm install
npm run dev

Environment Variables

Only PORT is actually read by the application. The full table, including which variables are declared-but-unused, is in INSTALL.md.

Backend:

  • PORT: 3000 — read by src/index.ts.
  • MAX_FILE_SIZE: declared in the Dockerfile and compose file, not enforced — the live multer config sets no limits.
  • CORS_ORIGIN: in backend/.env.example, not readapp.use(cors()) takes no options.
  • TEMP_DIR / /app/temp: created by the Dockerfile, never written to — processing is entirely in memory.

Frontend:

  • VITE_API_URL: not read. lib/api.ts picks its base URL from import.meta.env.DEV (http://localhost:3000/api in dev, /api in a build).

Development Workflow & Standards

Workflow

  1. Feature Branch: Create from main.
  2. Develop: Use hot-reload (npm run dev).
  3. Test: Perform manual testing of image operations and presets.
  4. Commit: Use type: description format (e.g., feat: add rotation).
  5. Merge: Merge into main after review.

Code Standards

  • TypeScript: Use strict types where possible.
  • Svelte: Keep components modular and under 300 lines.
  • Async/Await: Use for all asynchronous operations.
  • Semantic HTML: Ensure accessibility and proper structure.

Troubleshooting

More symptoms, including container-level ones, in INSTALL.md.

  • Port in use: lsof -ti:3000 | xargs kill -9
  • Sharp issues: npm rebuild sharp
  • Docker Cache: docker builder prune if builds fail unexpectedly.
  • Preview Glitches: Check browser console for Canvas API errors.

Last Updated: July 2026 — reconciled against the code during the docs standardization pass. Deployment and configuration detail now lives in INSTALL.md and UNRAID.md.