Files
pnger/docs/LIVE_PREVIEW_IMPLEMENTATION.md

366 lines
10 KiB
Markdown

# Live Preview Implementation Guide
## Overview
Live Preview is the #1 priority feature for PNGer. This guide outlines the implementation approach.
## Goals
1. **Instant Feedback**: Show preview within 100ms of parameter change
2. **Accurate Rendering**: Match final output as closely as possible
3. **Performance**: Don't block UI, handle large images efficiently
4. **Progressive**: Show low-quality preview immediately, high-quality after
## Architecture
### Approach: Hybrid Client + Server Preview
```
┌─────────────┐
│ Upload │
│ Image │
└──────┬──────┘
v
┌─────────────────────────────────┐
│ Client-Side Preview (Canvas) │ <-- Instant (< 100ms)
│ - Fast, approximate rendering │
│ - Uses browser native resize │
│ - Good for basic operations │
└─────────┬───────────────────────┘
v
┌─────────────────────────────────┐
│ Server Preview API (Optional) │ <-- Accurate (500ms-2s)
│ - Uses Sharp (same as export) │
│ - Exact output representation │
│ - Debounced to avoid spam │
└─────────────────────────────────┘
```
## Implementation Steps
### Phase 1: Client-Side Preview (Quick Win)
**Files to Modify:**
- `frontend/src/App.svelte`
- `frontend/src/lib/preview.ts` (new)
**Key Features:**
1. Canvas-based image rendering
2. Debounced updates (300ms after parameter change)
3. Show original and preview side-by-side
4. Display file size estimate
**Code Skeleton:**
```typescript
// frontend/src/lib/preview.ts
export async function generateClientPreview(
file: File,
options: TransformOptions
): Promise<string> {
return new Promise((resolve) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!
img.onload = () => {
// Calculate dimensions
const { width, height } = calculateDimensions(img, options);
canvas.width = width;
canvas.height = height;
// Apply transforms
if (options.fit === 'cover') {
drawCover(ctx, img, width, height, options.position);
} else {
ctx.drawImage(img, 0, 0, width, height);
}
// Apply filters (grayscale, blur, etc.)
applyFilters(ctx, options);
// Convert to data URL
const quality = options.quality / 100;
const dataUrl = canvas.toDataURL(`image/${options.format}`, quality);
resolve(dataUrl);
};
img.src = URL.createObjectURL(file);
});
}
```
**UI Updates:**
```svelte
<!-- App.svelte additions -->
<script lang="ts">
import { generateClientPreview } from './lib/preview';
import { debounce } from './lib/utils';
let previewUrl: string | null = null;
let originalSize: number = 0;
let previewSize: number = 0;
// Debounced preview generation
const updatePreview = debounce(async () => {
if (!file) return;
previewUrl = await generateClientPreview(file, {
width, height, quality, format, fit, position
});
// Calculate sizes
originalSize = file.size;
previewSize = estimateSize(previewUrl);
}, 300);
// Call on any parameter change
$: if (file) updatePreview();
</script>
<!-- Preview Section -->
{#if file && previewUrl}
<div class="preview-container">
<div class="image-comparison">
<div class="original">
<h3>Original</h3>
<img src={URL.createObjectURL(file)} alt="Original" />
<p>{formatFileSize(originalSize)}</p>
</div>
<div class="preview">
<h3>Preview</h3>
<img src={previewUrl} alt="Preview" />
<p>{formatFileSize(previewSize)}</p>
<p class="savings">
{calculateSavings(originalSize, previewSize)}
</p>
</div>
</div>
</div>
{/if}
```
### Phase 2: Server Preview API (Accurate)
**Files to Modify:**
- `backend/src/routes/image.ts`
**New Endpoint:**
```typescript
// POST /api/preview (returns base64 or temp URL)
router.post(
"/preview",
upload.single("file"),
async (req, res): Promise<void> => {
// Same processing as /transform
// But return as base64 data URL or temp storage URL
// Max preview size: 1200px (for performance)
const previewBuffer = await image.toBuffer();
const base64 = previewBuffer.toString('base64');
res.json({
preview: `data:image/${format};base64,${base64}`,
size: previewBuffer.length,
dimensions: { width: metadata.width, height: metadata.height }
});
}
);
```
**Benefits:**
- Exact rendering (uses Sharp like final output)
- Shows actual file size
- Handles complex operations client can't do
**Trade-offs:**
- Slower (network round-trip)
- Server load (mitigate with rate limiting)
### Phase 3: Progressive Loading
**Enhancement**: Show low-quality preview first, then high-quality
```typescript
// Generate two previews:
// 1. Immediate low-res (client-side, 200px max)
// 2. Delayed high-res (server-side, full resolution)
async function generateProgressivePreview() {
// Step 1: Fast low-res
const lowRes = await generateClientPreview(file, {
...options,
width: Math.min(options.width || 200, 200),
height: Math.min(options.height || 200, 200)
});
previewUrl = lowRes; // Show immediately
// Step 2: High-res from server (debounced)
const highRes = await fetchServerPreview(file, options);
previewUrl = highRes; // Replace when ready
}
```
## File Size Estimation
```typescript
function estimateSize(dataUrl: string): number {
// Base64 data URL size (approximate)
const base64Length = dataUrl.split(',')[1].length;
return Math.ceil((base64Length * 3) / 4); // Convert base64 to bytes
}
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
function calculateSavings(original: number, preview: number): string {
const diff = original - preview;
const percent = ((diff / original) * 100).toFixed(1);
if (diff > 0) return `${formatFileSize(diff)} saved (${percent}%)`;
if (diff < 0) return `${formatFileSize(-diff)} larger (${Math.abs(Number(percent))}%)`;
return 'Same size';
}
```
## UI/UX Considerations
### Layout Options
**Option A: Side-by-Side**
```
┌──────────────┬──────────────┐
│ Original │ Preview │
│ │ │
│ [Image] │ [Image] │
│ 2.4 MB │ 450 KB │
│ 1920x1080 │ 800x600 │
└──────────────┴──────────────┘
```
**Option B: Slider Compare**
```
┌────────────────────────────┐
│ [<──── Slider ────>] │
│ Original │ Preview │
│ │ │
└────────────────────────────┘
```
**Option C: Tabs**
```
┌─ Original ─┬─ Preview ─────┐
│ │
│ [Image] │
│ │
└─────────────────────────────┘
```
**Recommendation**: Start with Option A (simplest), add Option B later for detail comparison.
## Performance Optimizations
### 1. Debouncing
```typescript
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
```
### 2. Image Downsampling
- Preview max size: 1200px (retina displays)
- Original size only for final download
- Reduces memory usage and processing time
### 3. Worker Thread (Advanced)
- Offload canvas operations to Web Worker
- Keeps UI responsive during processing
```typescript
// preview.worker.ts
self.onmessage = async (e) => {
const { file, options } = e.data;
const preview = await generatePreview(file, options);
self.postMessage({ preview });
};
```
## Testing Plan
### Unit Tests
- [ ] `calculateDimensions()` with various aspect ratios
- [ ] `formatFileSize()` edge cases
- [ ] `debounce()` timing
### Integration Tests
- [ ] Preview updates on parameter change
- [ ] Preview matches final output (within tolerance)
- [ ] Large image handling (> 10MB)
- [ ] Multiple format conversions
### Manual Tests
- [ ] Mobile responsiveness
- [ ] Slow network simulation
- [ ] Various image formats (PNG, JPEG, WebP)
- [ ] Edge cases (1x1px, 10000x10000px)
## Rollout Strategy
### Step 1: Feature Flag
```typescript
// Enable via environment variable
const ENABLE_PREVIEW = import.meta.env.VITE_ENABLE_PREVIEW === 'true';
```
### Step 2: Beta Testing
- Deploy to staging environment
- Gather user feedback
- Monitor performance metrics
### Step 3: Gradual Rollout
- Enable for 10% of users
- Monitor error rates
- Full rollout if stable
## Success Metrics
- **User Engagement**: Time spent on page increases
- **Conversion**: More downloads completed
- **Performance**: Preview renders in < 500ms (p95)
- **Accuracy**: Preview matches output 95%+ of time
- **Satisfaction**: User feedback positive
## Future Enhancements
- [ ] Before/after slider with drag handle
- [ ] Zoom on preview (inspect details)
- [ ] Multiple preview sizes simultaneously
- [ ] A/B comparison (compare 2-4 settings)
- [ ] Preview history (undo/redo preview)
- [ ] Export preview settings as preset
---
**Estimated Effort**: 2-3 days for Phase 1 (client preview)
**Complexity**: Medium
**Impact**: ⭐⭐⭐⭐⭐ (Highest)
**Next Steps**:
1. Create feature branch `feature/live-preview`
2. Implement client-side preview
3. Add UI components
4. Test thoroughly
5. Merge to main
6. Deploy and monitor