File size: 2,359 Bytes
7bafae7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
 * Client-side image optimization.
 *
 * Resizes images and converts them to WebP before upload,
 * reducing bandwidth and storage costs significantly.
 */

interface OptimizedVariant {
  blob: Blob;
  width: number;
  height: number;
  suffix: string;
}

export interface OptimizedImage {
  original: { width: number; height: number };
  variants: OptimizedVariant[];
}

const VARIANTS = [
  { suffix: "thumb", maxWidth: 400, quality: 0.7 },
  { suffix: "medium", maxWidth: 1000, quality: 0.8 },
  { suffix: "full", maxWidth: 2000, quality: 0.85 },
] as const;

/**
 * Optimize an image file: resize to 3 WebP variants + extract dimensions.
 * Returns the variants sorted small to large.
 */
export async function optimizeImage(file: File): Promise<OptimizedImage> {
  const bitmap = await createImageBitmap(file);
  const { width: origW, height: origH } = bitmap;

  const variants: OptimizedVariant[] = [];

  for (const { suffix, maxWidth, quality } of VARIANTS) {
    // Skip variant if original is smaller than this tier
    if (origW <= maxWidth && suffix !== "full") continue;

    const scale = Math.min(1, maxWidth / origW);
    const w = Math.round(origW * scale);
    const h = Math.round(origH * scale);

    const canvas = new OffscreenCanvas(w, h);
    const ctx = canvas.getContext("2d");
    if (!ctx) throw new Error("Canvas 2D context unavailable");

    ctx.drawImage(bitmap, 0, 0, w, h);

    const blob = await canvas.convertToBlob({
      type: "image/webp",
      quality,
    });

    variants.push({ blob, width: w, height: h, suffix });
  }

  // Always ensure at least one variant (the full size)
  if (variants.length === 0) {
    const canvas = new OffscreenCanvas(origW, origH);
    const ctx = canvas.getContext("2d");
    if (!ctx) throw new Error("Canvas 2D context unavailable");

    ctx.drawImage(bitmap, 0, 0);

    const blob = await canvas.convertToBlob({
      type: "image/webp",
      quality: 0.85,
    });

    variants.push({ blob, width: origW, height: origH, suffix: "full" });
  }

  bitmap.close();

  return {
    original: { width: origW, height: origH },
    variants,
  };
}

/**
 * Check if the browser supports WebP encoding via OffscreenCanvas.
 */
export function supportsWebpOptimization(): boolean {
  try {
    return typeof OffscreenCanvas !== "undefined";
  } catch {
    return false;
  }
}