tfrere HF Staff commited on
Commit
7bafae7
·
1 Parent(s): 3171ebe

feat: add client-side image optimization (WebP variants)

Browse files

- Create image-optimizer.ts: resize images to 3 WebP variants
(thumb 400px, medium 1000px, full 2000px) using OffscreenCanvas
- Update upload.ts to optimize before uploading to backend
- Graceful fallback to original file if optimization unsupported

Made-with: Cursor

frontend/src/editor/image-optimizer.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Client-side image optimization.
3
+ *
4
+ * Resizes images and converts them to WebP before upload,
5
+ * reducing bandwidth and storage costs significantly.
6
+ */
7
+
8
+ interface OptimizedVariant {
9
+ blob: Blob;
10
+ width: number;
11
+ height: number;
12
+ suffix: string;
13
+ }
14
+
15
+ export interface OptimizedImage {
16
+ original: { width: number; height: number };
17
+ variants: OptimizedVariant[];
18
+ }
19
+
20
+ const VARIANTS = [
21
+ { suffix: "thumb", maxWidth: 400, quality: 0.7 },
22
+ { suffix: "medium", maxWidth: 1000, quality: 0.8 },
23
+ { suffix: "full", maxWidth: 2000, quality: 0.85 },
24
+ ] as const;
25
+
26
+ /**
27
+ * Optimize an image file: resize to 3 WebP variants + extract dimensions.
28
+ * Returns the variants sorted small to large.
29
+ */
30
+ export async function optimizeImage(file: File): Promise<OptimizedImage> {
31
+ const bitmap = await createImageBitmap(file);
32
+ const { width: origW, height: origH } = bitmap;
33
+
34
+ const variants: OptimizedVariant[] = [];
35
+
36
+ for (const { suffix, maxWidth, quality } of VARIANTS) {
37
+ // Skip variant if original is smaller than this tier
38
+ if (origW <= maxWidth && suffix !== "full") continue;
39
+
40
+ const scale = Math.min(1, maxWidth / origW);
41
+ const w = Math.round(origW * scale);
42
+ const h = Math.round(origH * scale);
43
+
44
+ const canvas = new OffscreenCanvas(w, h);
45
+ const ctx = canvas.getContext("2d");
46
+ if (!ctx) throw new Error("Canvas 2D context unavailable");
47
+
48
+ ctx.drawImage(bitmap, 0, 0, w, h);
49
+
50
+ const blob = await canvas.convertToBlob({
51
+ type: "image/webp",
52
+ quality,
53
+ });
54
+
55
+ variants.push({ blob, width: w, height: h, suffix });
56
+ }
57
+
58
+ // Always ensure at least one variant (the full size)
59
+ if (variants.length === 0) {
60
+ const canvas = new OffscreenCanvas(origW, origH);
61
+ const ctx = canvas.getContext("2d");
62
+ if (!ctx) throw new Error("Canvas 2D context unavailable");
63
+
64
+ ctx.drawImage(bitmap, 0, 0);
65
+
66
+ const blob = await canvas.convertToBlob({
67
+ type: "image/webp",
68
+ quality: 0.85,
69
+ });
70
+
71
+ variants.push({ blob, width: origW, height: origH, suffix: "full" });
72
+ }
73
+
74
+ bitmap.close();
75
+
76
+ return {
77
+ original: { width: origW, height: origH },
78
+ variants,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Check if the browser supports WebP encoding via OffscreenCanvas.
84
+ */
85
+ export function supportsWebpOptimization(): boolean {
86
+ try {
87
+ return typeof OffscreenCanvas !== "undefined";
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
frontend/src/editor/upload.ts CHANGED
@@ -1,8 +1,44 @@
 
 
 
 
 
1
  /**
2
  * Upload an image file to the backend.
3
- * Returns the URL to use in the editor (either HF dataset URL or local /uploads/ URL).
 
4
  */
5
  export async function uploadImage(file: File): Promise<string> {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  const form = new FormData();
7
  form.append("file", file);
8
 
 
1
+ import {
2
+ optimizeImage,
3
+ supportsWebpOptimization,
4
+ } from "./image-optimizer";
5
+
6
  /**
7
  * Upload an image file to the backend.
8
+ * Optimizes to WebP variants client-side before uploading.
9
+ * Returns the URL of the largest variant for use in the editor.
10
  */
11
  export async function uploadImage(file: File): Promise<string> {
12
+ // Try client-side optimization first
13
+ if (supportsWebpOptimization() && file.type.startsWith("image/")) {
14
+ try {
15
+ const optimized = await optimizeImage(file);
16
+ const urls: string[] = [];
17
+
18
+ for (const variant of optimized.variants) {
19
+ const ext = "webp";
20
+ const name = file.name.replace(/\.[^.]+$/, "");
21
+ const optimizedFile = new File(
22
+ [variant.blob],
23
+ `${name}-${variant.suffix}.${ext}`,
24
+ { type: "image/webp" }
25
+ );
26
+
27
+ const url = await uploadSingleFile(optimizedFile);
28
+ urls.push(url);
29
+ }
30
+
31
+ // Return the largest variant URL
32
+ return urls[urls.length - 1];
33
+ } catch (err) {
34
+ console.warn("[upload] Client-side optimization failed, uploading original:", err);
35
+ }
36
+ }
37
+
38
+ return uploadSingleFile(file);
39
+ }
40
+
41
+ async function uploadSingleFile(file: File): Promise<string> {
42
  const form = new FormData();
43
  form.append("file", file);
44