your personal website on atproto - mirror
blento.app
1import { getCDNImageBlobUrl, uploadBlob } from './methods';
2
3export function compressImage(
4 file: File | Blob,
5 maxSize: number = 900 * 1024,
6 maxDimension: number = 2048
7): Promise<{
8 blob: Blob;
9 aspectRatio: {
10 width: number;
11 height: number;
12 };
13}> {
14 return new Promise((resolve, reject) => {
15 const img = new Image();
16 const reader = new FileReader();
17
18 reader.onload = (e) => {
19 if (!e.target?.result) {
20 return reject(new Error('Failed to read file.'));
21 }
22 img.src = e.target.result as string;
23 };
24
25 reader.onerror = (err) => reject(err);
26 reader.readAsDataURL(file);
27
28 img.onload = () => {
29 let width = img.width;
30 let height = img.height;
31
32 // If image is already small enough, return original
33 if (file.size <= maxSize) {
34 console.log('skipping compression+resizing, already small enough');
35 return resolve({
36 blob: file,
37 aspectRatio: {
38 width,
39 height
40 }
41 });
42 }
43
44 if (width > maxDimension || height > maxDimension) {
45 if (width > height) {
46 height = Math.round((maxDimension / width) * height);
47 width = maxDimension;
48 } else {
49 width = Math.round((maxDimension / height) * width);
50 height = maxDimension;
51 }
52 }
53
54 // Create a canvas to draw the image
55 const canvas = document.createElement('canvas');
56 canvas.width = width;
57 canvas.height = height;
58 const ctx = canvas.getContext('2d');
59 if (!ctx) return reject(new Error('Failed to get canvas context.'));
60 ctx.drawImage(img, 0, 0, width, height);
61
62 // Use WebP for both compression and transparency support
63 let quality = 0.9;
64
65 function attemptCompression() {
66 canvas.toBlob(
67 (blob) => {
68 if (!blob) {
69 return reject(new Error('Compression failed.'));
70 }
71 if (blob.size <= maxSize || quality < 0.3) {
72 resolve({
73 blob,
74 aspectRatio: {
75 width,
76 height
77 }
78 });
79 } else {
80 quality -= 0.1;
81 attemptCompression();
82 }
83 },
84 'image/webp',
85 quality
86 );
87 }
88
89 attemptCompression();
90 };
91
92 img.onerror = (err) => reject(err);
93 });
94}
95
96export async function checkAndUploadImage(
97 recordWithImage: Record<string, any>,
98 key: string = 'image',
99 // e.g. /api/image-proxy?url=
100 imageProxy?: string
101) {
102 if (!recordWithImage[key]) return;
103
104 // Already uploaded as blob
105 if (typeof recordWithImage[key] === 'object' && recordWithImage[key].$type === 'blob') {
106 return;
107 }
108
109 if (typeof recordWithImage[key] === 'string' && imageProxy) {
110 const proxyUrl = imageProxy + encodeURIComponent(recordWithImage[key]);
111 const response = await fetch(proxyUrl);
112 if (!response.ok) {
113 throw Error('failed to get image from image proxy');
114 }
115
116 const blob = await response.blob();
117 const compressedBlob = await compressImage(blob);
118
119 recordWithImage[key] = await uploadBlob({ blob: compressedBlob.blob });
120
121 return;
122 }
123
124 if (recordWithImage[key]?.blob) {
125 if (recordWithImage[key].objectUrl) {
126 URL.revokeObjectURL(recordWithImage[key].objectUrl);
127 }
128 const compressedBlob = await compressImage(recordWithImage[key].blob);
129 recordWithImage[key] = await uploadBlob({ blob: compressedBlob.blob });
130 }
131}
132
133export function getImageFromRecord(
134 recordWithImage: Record<string, any> | undefined,
135 did: string,
136 key: string = 'image'
137): string | undefined {
138 if (!recordWithImage?.[key]) return;
139
140 if (typeof recordWithImage[key] === 'object' && recordWithImage[key].$type === 'blob') {
141 return getCDNImageBlobUrl({ did, blob: recordWithImage[key] });
142 }
143
144 if (recordWithImage[key].objectUrl) return recordWithImage[key].objectUrl;
145
146 if (recordWithImage[key].blob) {
147 recordWithImage[key].objectUrl = URL.createObjectURL(recordWithImage[key].blob);
148 return recordWithImage[key].objectUrl;
149 }
150
151 return recordWithImage[key];
152}