Image Editor: Non-Destructive Canvas Editing
Complete guide to the SveltyCMS Image Editor β covering the editing workflow for creators, widget integration for developers, Web Worker offloading, compare slider, touch gestures, and the server-side Sharp.js baking pipeline.
On this page
The SveltyCMS Image Editor gives you professional-grade media manipulation directly inside the CMS. Every edit you make is applied to a high-resolution original on the server β never to a degraded browser preview. Your original files always stay safe.
π€ For Content Creators & Admins
The Goal
You need to crop, adjust, annotate, or filter an image thatβs already been uploaded to the Media Gallery β or thatβs attached to a content entry via the MediaUpload widget.
How to Edit an Image
From the Media Gallery
- Go to Media Gallery in the main navigation.
- Hover over any image thumbnail β action buttons appear in the top-right corner.
- Click the pencil icon (βοΈ) to launch the Image Editor with that image pre-loaded.
- Make your edits using the tools in the bottom toolbar.
- Click Save (or press
Ctrl+S/βS) β your edits are sent to the server and applied.
From a Content Entry (Widget)
- In any content form, upload or select an image using the MediaUpload widget.
- Click the Edit button on the preview to open the editor.
- After saving, the edited version replaces the displayed image in that entry.
Core Editing Tools
| Tool | What It Does |
|---|---|
| Crop | Trim edges, lock to 1:1 / 4:3 / 16:9 ratios, or free-form. Rule-of-thirds grid overlay built-in. |
| Focal | Set the βfocus pointβ of your image β ensures key subjects stay visible in responsive layouts. |
| Rotate | Rotate 90Β° left/right or flip horizontally/vertically. |
| FineTune | Adjust brightness, contrast, saturation, temperature, and more. |
| Blur | Paint blur regions over sensitive information or backgrounds. | | Annotate | Draw arrows, rectangles, or add text directly onto the image. | | Watermark | Apply a preset watermark with adjustable opacity and position. |
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
Mod + S |
Save Changes: Finalizes edits and triggers server-side baking. |
Mod + Z |
Undo: Reverts the last state change. |
Mod + Shift + Z |
Redo: Re-applies the next state change. |
Escape |
Cancel: Discards current edits and closes the editor. |
+ / = |
Zoom in: Increase canvas magnification. |
- |
Zoom out: Decrease canvas magnification. |
0 |
Reset zoom: Return to 100% and center the image. |
All shortcuts are displayed on buttons via aria-keyshortcuts and are discoverable through tooltips.
Compare Mode
Click the Compare button in the toolbar to toggle a split-screen view β original on the left, edited on the right β so you can see exactly what changed.
π οΈ For Developers & Engineers
Architecture
The editor is built on a modular widget system where each tool registers itself via import.meta.glob auto-discovery. Pixel-level processing runs on a Web Worker; server-side baking runs via Sharp.js.
ββ Browser (Svelte 5) βββββββββββββββββββββββββββββββββββββββββββ
β editor.svelte βββ image-editor-store.svelte.ts β
β β β
β editor-canvas.svelte βββ filter.worker.ts (Web Worker) β
β β β
β ββββββ΄βββββββββββββββββββββββββββββββββββββββββββ β
β β Crop β Blur β FineTune β Annotate β Watermark β β
β β FocalPoint β Rotate β Zoom (8 widgets) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β JSON instructions (POST /api/media/manipulate/:id)
ββ Server (Sharp.js) βββ΄βββββββββββββββββββββββββββββββββββββββββ
β Sharp.js Pipeline β WebP + AVIF variants β Media Store β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Canvas Engine: Modern & Lightweight
- Engine: Powered by svelte-canvas, a thin reactive wrapper for the native Web Canvas API.
- Bundle Size: ~8KB gzipped β a 90%+ reduction vs. the 500KB+ Konva.js dependency.
- Svelte 5 Runes: Uses
$state()for deep reactivity,$derived()for computed values, and$effect()for side effects. No legacy stores.
Web Worker: Offloaded Pixel Processing
Sharpness and blur convolution (3Γ3 kernel) run on a dedicated Web Worker thread to keep the main UI at 60fps during slider adjustments. The worker lives at src/components/image-editor/workers/filter.worker.ts and is initialized automatically on canvas mount.
- What it handles:
applySharpness()β unsharp mask sharpening and box blur softening.buildFilterString()β CSS filter string assembly. - Zero-copy transfer: Processed
ImageDatabuffers are transferred viapostMessage(buffer, [buffer])for minimal latency. - Graceful fallback: If worker creation fails (e.g., CSP restrictions), processing falls back to the main thread silently.
- Performance: A 1024Γ768 image with sharpness = 36 processes in ~42ms on the worker thread vs. ~120ms on the main thread.
Touch Gestures: Mobile-First Canvas Control
The canvas supports native multi-touch gestures for zoom and pan on tablets and phones:
- Pinch-to-zoom: Two fingers scale the viewport around the midpoint (1.5px dead zone to prevent jitter).
- Two-finger pan: Translates the canvas simultaneously during pinch.
- Single-finger tool delegation: Routes touch events to the active toolβs mouse handlers (e.g., drag crop handles).
Compare Slider: Split-Screen Preview
Toggle a vertical split-screen view to compare the original (left half) vs. edited (right half).
- Activate: Click the βCompareβ button in the bottom toolbar.
- Divider: A dashed white line separates the two halves.
- State: Stored as
imageEditorStore.compareSliderPositionβ auto-clamps to the 0β100 range (0 = off, 1β100 = split percentage). - Programmatic access: Set
imageEditorStore.compareSliderPosition = 50to enable at 50%.
Widget System: Auto-Discovery
Tools are automatically discovered from src/components/image-editor/widgets/ using import.meta.glob. Each tool exports an EditorWidget shape:
// src/components/image-editor/widgets/crop/index.ts
import type { Component } from "svelte";
import Tool from "./tool.svelte";
export default {
key: "crop",
title: "Crop",
icon: "mdi:crop",
tool: Tool as unknown as Component<Record<string, unknown>>,
};
Adding a new tool:
- Create a folder in
src/components/image-editor/widgets/your-tool/. - Add an
index.tswith the default export matching theEditorWidgetinterface. - Create
tool.sveltefor the canvas interaction logic. - It appears automatically in the editor sidebar β no manual registration.
Plugin Extensibility (image_editor_tool Zone)
Plugins can register custom editing tools via the image_editor_tool injection zone. These appear alongside built-in widgets in the editor sidebar and receive the current editor context (activeState, onToolSelect, hasImage) as props.
Registering a plugin tool:
// src/plugins/my-image-tool/index.ts
export default {
metadata: { id: "my-image-tool", name: "My Image Tool", version: "1.0.0", enabled: true },
ui: {
slots: [
{
id: "my-editor-tool",
zone: "image_editor_tool",
component: () => import("./editor-tool.svelte"),
},
],
},
};
Tool component receives these props:
activeStateβ currently active tool key (string)onToolSelectβ function to activate a tool(toolKey: string) => voidhasImageβ whether an image is loaded (boolean)
Server-Side Baking (Sharp.js)
Note: The image editorβs primary image processing (filters, convolution, color adjustments) is handled client-side in a Web Worker for instant feedback. The server-side step described below is an optional persistence layer that re-applies lossless transformations to the original asset β it is not the primary processing path.
Instead of exporting a lossy blob from the browser, the editor sends a JSON instruction set to the server:
POST /api/media/manipulate/:mediaId
Content-Type: application/json
{
"rotation": 90,
"flipH": false,
"flipV": false,
"crop": { "x": 100, "y": 50, "width": 800, "height": 600 },
"filters": { "brightness": 10, "contrast": 5, "saturation": 0 },
"focalPoint": { "x": 35, "y": 65 },
"saveBehavior": "new",
"blurRegions": [],
"watermarks": [],
"annotations": []
}
The server uses Sharp.js to apply these transformations to the original, uncompressed asset and returns the new variant. This guarantees:
- Zero-loss quality: No browser canvas recompression artifacts.
- Non-destructive: The original is never modified β edited versions are saved as linked variants.
- Multi-format: WebP and AVIF variants are generated automatically.
Integration: MediaUpload Widget
The MediaUpload widget (in src/widgets/core/media-upload/) embeds the editor directly:
<!-- Inside media-upload.svelte -->
{#if showEditor}
<ImageEditorModal
image={value}
{watermarkPreset}
onsave={handleEditorSave}
close={() => (showEditor = false)}
/>
{/if}
To integrate the editor in a custom widget:
<script lang="ts">
import ImageEditorModal from '@src/components/image-editor/image-editor-modal.svelte';
let showEditor = $state(false);
async function handleEditorSave(detail: any) {
const { mediaId, manipulations } = detail;
const response = await fetch(`/api/media/manipulate/${mediaId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(manipulations)
});
const result = await response.json();
// Update your local state with result.data
}
</script>
<button onclick={() => (showEditor = true)}>Edit Image</button>
{#if showEditor}
<ImageEditorModal
image={someMediaObject} <!-- MediaImage | File | string -->
onsave={handleEditorSave}
close={() => (showEditor = false)}
/>
{/if}
ImageEditorModal Props
| Prop | Type | Required | Description |
|---|---|---|---|
image |
MediaImage \| File \| string |
Yes | The image to edit. Pass a URL string, File object, or MediaImage with _id. |
watermarkPreset |
WatermarkOptions \| null |
No | Pre-configured watermark to auto-apply. |
onsave |
(detail: SaveDetail) => void |
Yes | Called with { mediaId, manipulations, focalPoint, saveBehavior }. |
close |
() => void |
Yes | Called when the user cancels or closes the editor. |
State Management
The image-editor-store.svelte.ts manages all editor state with Svelte 5 runes:
import { imageEditorStore } from "@src/stores/image-editor-store.svelte";
// Core state (reactive via $state)
imageEditorStore.state.zoom; // number (0.1β5)
imageEditorStore.state.rotation; // number (degrees)
imageEditorStore.state.crop; // { x, y, width, height } | null
imageEditorStore.state.filters; // Record<string, number>
imageEditorStore.state.focalPoint; // { x: number, y: number }
imageEditorStore.state.activeState; // string β current tool key
// Compare slider (0 = off, 1β100 = split position)
imageEditorStore.compareSliderPosition; // number (get/set, auto-clamped)
// Derived
imageEditorStore.canUndoState; // boolean
imageEditorStore.canRedoState; // boolean
// Actions
imageEditorStore.setImageElement(img); // Set the source image
imageEditorStore.switchTool("crop"); // Activate a tool
imageEditorStore.takeSnapshot(); // Save undo point
imageEditorStore.handleUndo(); // Undo
imageEditorStore.handleRedo(); // Redo
imageEditorStore.reset(); // Clear all state
Progressive Loading & Smart Shimmer
For every processed image, the server pipeline extracts:
- Dominant Color: The primary RGB color, used as a shimmer background in the Media Gallery.
- Tiny Placeholder: A 32Γ32px ultra-low-quality WebP base64 string for instant previews before the full image loads.
These are stored in media.metadata and consumed by the MediaGrid component.
βΏ Accessibility (WCAG 2.2 AA / WCAG 3.0)
The Image Editor is built for creators of all abilities:
- Keyboard Navigation: Arrow keys navigate between tool buttons. Enter/Space activates a tool. All crop handles are focusable and adjustable via arrow keys. Home/End jump to first/last tool. Zoom shortcuts:
+/-/0. - ARIA Integration: The editor modal uses
role="dialog"witharia-modal="true"and a focus trap. The sidebar usesrole="tablist"withorientation="vertical"; tool buttons userole="tab"witharia-selectedandaria-posinset/aria-setsize. The toolbar usesrole="toolbar". Status changes are announced viaaria-live="assertive". - Focus Management: When the editor opens, focus moves to the modal container. TAB loops inside the modal (focus trap). When closing, focus returns to the trigger element (Media Gallery or widget).
Mod+Ssaves,Escapecloses with confirmation. - Contrast: Primary actions (Save button) exceed 12:1 contrast ratio. Chrome text uses subdued opacity typical of professional dark editing UIs (similar to Lightroom / Pintura). All functional UI elements have accessible names via
aria-labelor visible text. - Touch Accessibility: Pinch-to-zoom and two-finger pan support on mobile/tablet devices.
- CSS Architecture: Styling uses 100% Tailwind CSS v4 utilities inline β zero external CSS files. Design tokens (
--editor-chrome-*custom properties) are defined in a single component for consistent theming.
ATAG 2.0 Compliance (Authoring Tools)
Since SveltyCMS is an authoring tool:
- Accessible UI: The editor interface itself is keyboard-navigable (WCAG 2.2 AA).
- Support Accessible Content: The MediaUpload widget prompts for alt text when
altText: trueis configured in the field schema. Edited images retain their alt text through the manipulation pipeline.
π§ͺ Testing
The image editor is covered by 73 unit tests across four test files:
| Test File | Tests | Coverage |
|---|---|---|
tests/unit/stores/image-editor-store.test.ts |
26 | Undo/redo, tool switching, compare slider, zoom, rotation, reset, save behavior, error handling |
tests/unit/stores/filter-worker.test.ts |
12 | buildFilterString(), applySharpness() (no-op, sharpen, blur, edge cases, 1Γ1, 1024Γ768 performance) |
tests/unit/components/image-editor/widget-registry.test.ts |
27 | Widget validation (12 cases), defaults (8), sorting (3), disabled filtering (2), category + key lookup |
tests/unit/components/image-editor/plugin-tool-slot.test.ts |
8 | InjectionZone acceptance, slot registration, zone isolation, server actions, props, condition filtering |
Run with:
bun test tests/unit/stores/image-editor-store.test.ts tests/unit/stores/filter-worker.test.ts tests/unit/components/image-editor/widget-registry.test.ts tests/unit/components/image-editor/plugin-tool-slot.test.ts
π Related Documentation
- Media API Reference β Full technical spec for the
/api/media/manipulateendpoint. - MediaUpload Widget β Widget documentation with field configuration examples.
- Accessibility Guide β WCAG compliance requirements and testing procedures.
- Contributing Docs β Documentation standards and PR workflow.