# 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 { 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 {#if file && previewUrl}

Original

Original

{formatFileSize(originalSize)}

Preview

Preview

{formatFileSize(previewSize)}

{calculateSavings(originalSize, previewSize)}

{/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 => { // 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 any>( func: T, wait: number ): (...args: Parameters) => void { let timeout: ReturnType; return (...args: Parameters) => { 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