File size: 2,659 Bytes
899d9c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1778c9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
93
94
95
96
97
98
99
100
101
102
103
export function fileToDataURL(file: File): Promise<string> {
	return new Promise((resolve, reject) => {
		const reader = new FileReader();

		reader.onload = function (event) {
			resolve(event.target?.result as string);
		};

		reader.onerror = function (error) {
			reject(error);
		};

		reader.readAsDataURL(file);
	});
}

interface CompressBase64Options {
	base64: string;
	maxSizeKB: number;
	outputFormat?: string; // 'image/jpeg' | 'image/webp'
	minQuality?: number; // default: 0.1
	maxQuality?: number; // default: 1.0
	maxIterations?: number; // default: 10
}

export async function compressBase64Image(options: CompressBase64Options): Promise<string> {
	const {
		base64,
		maxSizeKB,
		outputFormat = "image/jpeg",
		minQuality = 0.1,
		maxQuality = 1.0,
		maxIterations = 10,
	} = options;

	const img = await new Promise<HTMLImageElement>((resolve, reject) => {
		const image = new Image();
		image.crossOrigin = "Anonymous";
		image.onload = () => resolve(image);
		image.onerror = reject;
		image.src = base64;
	});

	const canvas = document.createElement("canvas");
	canvas.width = img.width;
	canvas.height = img.height;
	const ctx = canvas.getContext("2d");
	if (!ctx) throw new Error("Could not get canvas context");
	ctx.drawImage(img, 0, 0);

	let minQ = minQuality;
	let maxQ = maxQuality;
	let bestBase64 = "";

	for (let i = 0; i < maxIterations; i++) {
		const q = (minQ + maxQ) / 2;
		const b64 = canvas.toDataURL(outputFormat, q);
		const size = getBase64ImageSize(b64).kilobytes;

		if (size > maxSizeKB) {
			maxQ = q;
		} else {
			minQ = q;
			bestBase64 = b64;
		}
	}

	// If no quality produced a small enough image, return the lowest quality result
	if (!bestBase64) {
		bestBase64 = canvas.toDataURL(outputFormat, minQuality);
	}

	return bestBase64;
}

/**
 * Get the size of a Base64 image string in bytes and kilobytes.
 * @param base64 - The Base64 image string (with or without data URL prefix).
 * @returns { bytes: number, kilobytes: number, megabytes: number }
 */
export function getBase64ImageSize(base64: string): { bytes: number; kilobytes: number; megabytes: number } {
	// Remove data URL prefix if present
	const cleanedBase64 = base64.split(",")[1] || base64;

	// Calculate padding
	const padding = (cleanedBase64.match(/=+$/) || [""])[0].length;

	// Calculate size in bytes
	const bytes = (cleanedBase64.length * 3) / 4 - padding;

	// Convert to kilobytes
	const kilobytes = bytes / 1024;

	// Convert to megabytes (optional)
	const megabytes = kilobytes / 1024;

	return {
		bytes: Math.round(bytes),
		kilobytes: parseFloat(kilobytes.toFixed(2)),
		megabytes: parseFloat(megabytes.toFixed(2)),
	};
}