mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(ui): clean up settings, automate page, fullscreen logo, and README
- README: simplify to match Stirling-PDF style, add dashboard screenshot - Settings: remove unsupported languages from dropdown, remove unused experimental tools toggle - Automate: remove hardcoded template pipelines from sidebar - Fullscreen: add GemLogo icon to header
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
<p align="center">
|
||||
<h1 align="center">Stirling Image</h1>
|
||||
<p align="center">The Open-Source Image Processing Platform</p>
|
||||
<img src="apps/web/public/logo-192.png" width="80" alt="Stirling Image logo">
|
||||
</p>
|
||||
|
||||
<h1 align="center">Stirling Image - The Open-Source Image Processing Platform</h1>
|
||||
|
||||
Stirling Image is a powerful, open-source image processing platform. Self-host it in a single Docker container with a private API. Resize, compress, convert, remove backgrounds, upscale, run OCR, and more — without sending images to external services.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/siddharthksah/Stirling-Image/pkgs/container/stirling-image"><img src="https://img.shields.io/badge/Docker-ghcr.io-blue?logo=docker" alt="Docker"></a>
|
||||
<a href="https://github.com/siddharthksah/Stirling-Image/actions"><img src="https://img.shields.io/github/actions/workflow/status/siddharthksah/Stirling-Image/ci.yml?label=CI" alt="CI"></a>
|
||||
@@ -10,114 +13,37 @@
|
||||
<a href="https://github.com/siddharthksah/Stirling-Image/stargazers"><img src="https://img.shields.io/github/stars/siddharthksah/Stirling-Image?style=social" alt="Stars"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||

|
||||
|
||||
Self-hosted image processing with 33+ tools in a single Docker container. Resize, compress, convert, watermark, remove backgrounds, run OCR, and more. Nothing leaves your server.
|
||||
## Key Capabilities
|
||||
|
||||
Inspired by [Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF), built for images.
|
||||
- **33+ image tools** — Resize, crop, compress, convert, watermark, OCR, and more.
|
||||
- **AI-powered** — Background removal, upscaling, object erasing, face blurring — all running locally.
|
||||
- **Automation & workflows** — Chain tools into reusable pipelines. Batch process up to 200 images at once.
|
||||
- **Developer platform** — REST API for every tool. Swagger docs included.
|
||||
- **Your data stays yours** — No telemetry, no tracking, no cloud. Single Docker container on any architecture.
|
||||
|
||||
<!-- TODO: Add screenshot here -->
|
||||
<!--  -->
|
||||
For a full feature list, see the docs: **https://siddharthksah.github.io/Stirling-Image/**
|
||||
|
||||
## Quick start
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
docker run -d -p 1349:1349 -v ./data:/data ghcr.io/siddharthksah/stirling-image:latest
|
||||
docker run -d -p 1349:1349 -v stirling-data:/data ghcr.io/siddharthksah/stirling-image:latest
|
||||
```
|
||||
|
||||
Open [http://localhost:1349](http://localhost:1349). Default login is `admin` / `admin`.
|
||||
Then open: http://localhost:1349. Default login: `admin` / `admin`.
|
||||
|
||||
## What it does
|
||||
For full installation options, see the [Getting Started Guide](https://siddharthksah.github.io/Stirling-Image/guide/getting-started).
|
||||
|
||||
- **33+ image tools** in one place — resize, crop, rotate, compress, convert, watermark, color adjustments, and the rest.
|
||||
## Resources
|
||||
|
||||
- **AI tools that run locally** — background removal (rembg), upscaling (Real-ESRGAN), OCR (PaddleOCR), face blurring (MediaPipe), object erasing (LaMa). No external API calls.
|
||||
- [**Documentation**](https://siddharthksah.github.io/Stirling-Image/)
|
||||
- [**API Docs**](https://siddharthksah.github.io/Stirling-Image/api/rest)
|
||||
- [**Configuration**](https://siddharthksah.github.io/Stirling-Image/guide/configuration)
|
||||
|
||||
- **Your hardware, your data** — no telemetry, no tracking, no cloud. Files stay on your machine.
|
||||
## Support
|
||||
|
||||
- **Batch processing** — drop up to 200 images, apply any tool, get a ZIP back. Configurable concurrency.
|
||||
|
||||
- **Pipelines** — chain tools into reusable workflows (resize, then compress, then convert to WebP, then strip metadata). Save them and rerun later.
|
||||
|
||||
- **REST API** — every tool is exposed at `/api/v1/tools/:toolId`. Swagger docs at `/api/docs`.
|
||||
|
||||
- **Persistent file storage** — save processed images server-side with version tracking. Pick up where you left off.
|
||||
|
||||
- **Teams and admin settings** — manage users, toggle tool visibility, configure cleanup, upload a custom logo.
|
||||
|
||||
- **Single container** — runs on Intel, AMD, and Apple Silicon (`linux/amd64` + `linux/arm64`).
|
||||
|
||||
## Tools
|
||||
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
| **Essentials** | Resize, Crop, Rotate & Flip, Convert, Compress |
|
||||
| **Optimization** | Strip Metadata, Bulk Rename, Image to PDF, Favicon Generator |
|
||||
| **Adjustments** | Brightness/Contrast, Saturation, Color Channels, Color Effects, Replace Color |
|
||||
| **AI Tools** | Background Removal, Upscaling, Object Eraser, OCR, Face Blur, Smart Crop |
|
||||
| **Watermark** | Text Watermark, Image Watermark, Text Overlay, Image Composition |
|
||||
| **Utilities** | Image Info, Compare, Find Duplicates, Color Palette, QR Generator, Barcode Reader |
|
||||
| **Layout** | Collage/Grid, Image Splitting, Border & Frame |
|
||||
| **Format** | SVG to Raster, Image to SVG, GIF Tools |
|
||||
| **Automation** | Pipeline Builder, Batch Processing |
|
||||
|
||||
## Supported formats
|
||||
|
||||
**In:** JPG, PNG, WebP, AVIF, TIFF, BMP, GIF (animated), SVG, HEIC/HEIF, JPEG XL, ICO, RAW (CR2, NEF, ARW, DNG)
|
||||
|
||||
**Out:** JPG, PNG, WebP, AVIF, TIFF, GIF, JPEG XL, SVG, ICO, PDF
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PORT` | `1349` | Server port |
|
||||
| `AUTH_ENABLED` | `true` | Require login (`admin` / `admin` by default) |
|
||||
| `MAX_UPLOAD_SIZE_MB` | `100` | Max file upload size |
|
||||
| `MAX_BATCH_SIZE` | `200` | Max files per batch |
|
||||
| `CONCURRENT_JOBS` | `3` | Parallel processing limit |
|
||||
| `FILE_MAX_AGE_HOURS` | `24` | Auto-delete temp files after this many hours |
|
||||
| `FILES_STORAGE_PATH` | `./data/files` | Where persistent user files are stored |
|
||||
| `STORAGE_MODE` | `local` | Storage backend (`local` or `s3`) |
|
||||
|
||||
See [`.env.example`](.env.example) for the full list.
|
||||
|
||||
## Docker Compose example
|
||||
|
||||
```yaml
|
||||
services:
|
||||
stirling-image:
|
||||
image: ghcr.io/siddharthksah/stirling-image:latest
|
||||
container_name: stirling-image
|
||||
ports:
|
||||
- "1349:1349"
|
||||
volumes:
|
||||
- stirling-data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
stirling-data:
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/siddharthksah/Stirling-Image.git
|
||||
cd Stirling-Image
|
||||
pnpm install
|
||||
pnpm dev
|
||||
# UI: http://localhost:1349
|
||||
```
|
||||
|
||||
Requires Node.js 22+ and pnpm 9+.
|
||||
|
||||
## Tech stack
|
||||
|
||||
React 19 + Vite frontend, Fastify + Sharp backend, SQLite via Drizzle ORM, Python sidecar for AI/ML models. Monorepo with pnpm workspaces. Multi-arch Docker builds.
|
||||
|
||||
## Support this project
|
||||
|
||||
If you find this useful, consider supporting development:
|
||||
- **Bug Reports**: [GitHub Issues](https://github.com/siddharthksah/Stirling-Image/issues)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/sponsors/siddharthksah"><img src="https://img.shields.io/badge/Sponsor-GitHub-ea4aaa?logo=github-sponsors" alt="GitHub Sponsors"></a>
|
||||
|
||||
@@ -397,12 +397,7 @@ function SystemSection() {
|
||||
onChange={(e) => updateSetting("defaultLocale", e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg border border-border bg-background text-sm text-foreground"
|
||||
>
|
||||
<option value="en">English (en)</option>
|
||||
<option value="es">Spanish (es)</option>
|
||||
<option value="fr">French (fr)</option>
|
||||
<option value="de">German (de)</option>
|
||||
<option value="zh">Chinese (zh)</option>
|
||||
<option value="ja">Japanese (ja)</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</SettingRow>
|
||||
|
||||
@@ -420,32 +415,6 @@ function SystemSection() {
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
label="Enable Experimental Tools"
|
||||
description="Show tools that are still in development. These may be unstable."
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateSetting(
|
||||
"enableExperimentalTools",
|
||||
settings.enableExperimentalTools === "true" ? "false" : "true",
|
||||
)
|
||||
}
|
||||
className={cn(
|
||||
"w-11 h-6 rounded-full transition-colors relative",
|
||||
settings.enableExperimentalTools === "true" ? "bg-primary" : "bg-muted-foreground/30",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"block w-4 h-4 rounded-full bg-white absolute top-1 transition-transform",
|
||||
settings.enableExperimentalTools === "true" ? "translate-x-6" : "translate-x-1",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</SettingRow>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3">File Management</h4>
|
||||
</div>
|
||||
|
||||
@@ -1,74 +1,7 @@
|
||||
import { Globe, Play, ShieldOff, Stamp, Trash2, User, Workflow, Zap } from "lucide-react";
|
||||
import { Play, Trash2, Workflow } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { AppLayout } from "@/components/layout/app-layout";
|
||||
import { PipelineBuilder, type PipelineStep } from "@/components/tools/pipeline-builder";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** Pipeline template definition. */
|
||||
interface PipelineTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
color: string;
|
||||
steps: Array<{ toolId: string; settings: Record<string, unknown> }>;
|
||||
}
|
||||
|
||||
const TEMPLATES: PipelineTemplate[] = [
|
||||
{
|
||||
name: "Social Media Ready",
|
||||
description: "Resize 1080x1080, compress 200KB, strip metadata, convert to WebP",
|
||||
icon: Globe,
|
||||
color: "bg-blue-500/10 text-blue-500",
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 1080, height: 1080, fit: "cover" } },
|
||||
{ toolId: "compress", settings: { quality: 80 } },
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "convert", settings: { format: "webp" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Privacy Clean",
|
||||
description: "Strip all metadata and convert to JPG",
|
||||
icon: ShieldOff,
|
||||
color: "bg-green-500/10 text-green-500",
|
||||
steps: [
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "convert", settings: { format: "jpg" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Web Optimization",
|
||||
description: "Resize to 1920px, convert to WebP, compress to 80% quality",
|
||||
icon: Zap,
|
||||
color: "bg-yellow-500/10 text-yellow-500",
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 1920, fit: "inside" } },
|
||||
{ toolId: "convert", settings: { format: "webp" } },
|
||||
{ toolId: "compress", settings: { quality: 80 } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Profile Picture",
|
||||
description: "Resize to 400x400 and compress",
|
||||
icon: User,
|
||||
color: "bg-purple-500/10 text-purple-500",
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 400, height: 400, fit: "cover" } },
|
||||
{ toolId: "compress", settings: { quality: 85 } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Watermark Batch",
|
||||
description: "Add text watermark, strip metadata, and compress",
|
||||
icon: Stamp,
|
||||
color: "bg-red-500/10 text-red-500",
|
||||
steps: [
|
||||
{ toolId: "watermark-text", settings: { text: "SAMPLE", opacity: 0.3 } },
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "compress", settings: { quality: 85 } },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface SavedPipeline {
|
||||
id: string;
|
||||
@@ -200,17 +133,6 @@ export function AutomatePage() {
|
||||
[steps],
|
||||
);
|
||||
|
||||
// Load template into builder
|
||||
const loadTemplate = useCallback((template: PipelineTemplate) => {
|
||||
const newSteps: PipelineStep[] = template.steps.map((s) => ({
|
||||
id: crypto.randomUUID(),
|
||||
toolId: s.toolId,
|
||||
settings: { ...s.settings },
|
||||
}));
|
||||
setSteps(newSteps);
|
||||
setExecutionResult(null);
|
||||
}, []);
|
||||
|
||||
// Load saved pipeline into builder
|
||||
const loadSaved = useCallback((pipeline: SavedPipeline) => {
|
||||
const newSteps: PipelineStep[] = pipeline.steps.map((s) => ({
|
||||
@@ -225,35 +147,9 @@ export function AutomatePage() {
|
||||
return (
|
||||
<AppLayout showToolPanel={false}>
|
||||
<div className="flex h-full w-full overflow-hidden">
|
||||
{/* Left sidebar: templates + saved */}
|
||||
<div className="w-72 border-r border-border overflow-y-auto p-4 space-y-6 shrink-0 hidden md:block">
|
||||
{/* Templates */}
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-3">
|
||||
Templates
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{TEMPLATES.map((tpl) => (
|
||||
<button
|
||||
key={tpl.name}
|
||||
type="button"
|
||||
onClick={() => loadTemplate(tpl)}
|
||||
className="w-full text-left p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className={cn("p-1.5 rounded-md", tpl.color)}>
|
||||
<tpl.icon className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-foreground">{tpl.name}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">{tpl.description}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Saved automations */}
|
||||
{savedPipelines.length > 0 && (
|
||||
{/* Left sidebar: saved automations */}
|
||||
{savedPipelines.length > 0 && (
|
||||
<div className="w-72 border-r border-border overflow-y-auto p-4 space-y-6 shrink-0 hidden md:block">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-3">
|
||||
Saved Automations
|
||||
@@ -293,8 +189,8 @@ export function AutomatePage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main builder area */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as icons from "lucide-react";
|
||||
import { Eye, EyeOff, FileImage, LayoutGrid, List, Search } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { GemLogo } from "@/components/common/gem-logo";
|
||||
import { apiGet } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -63,7 +64,11 @@ export function FullscreenGridPage() {
|
||||
{/* Top bar */}
|
||||
<header className="sticky top-0 z-30 bg-background/95 backdrop-blur-sm border-b border-border">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-3 flex items-center gap-4">
|
||||
<Link to="/" className="text-lg font-bold text-foreground shrink-0">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2 text-lg font-bold text-foreground shrink-0"
|
||||
>
|
||||
<GemLogo className="h-6 w-6 text-primary" />
|
||||
Stirling <span className="text-primary">Image</span>
|
||||
</Link>
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Reference in New Issue
Block a user