Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/imageUtils.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { decodeBase64, VSBuffer } from '../../../../base/common/buffer.js';
7
import { joinPath } from '../../../../base/common/resources.js';
8
import { URI } from '../../../../base/common/uri.js';
9
import { IFileService } from '../../../../platform/files/common/files.js';
10
import { ILogService } from '../../../../platform/log/common/log.js';
11
12
/**
13
* Resizes an image provided as a UInt8Array string. Resizing is based on Open AI's algorithm for tokenzing images.
14
* https://platform.openai.com/docs/guides/vision#calculating-costs
15
* @param data - The UInt8Array string of the image to resize.
16
* @returns A promise that resolves to the UInt8Array string of the resized image.
17
*/
18
19
export async function resizeImage(data: Uint8Array | string, mimeType?: string): Promise<Uint8Array> {
20
const isGif = mimeType === 'image/gif';
21
22
if (typeof data === 'string') {
23
data = convertStringToUInt8Array(data);
24
}
25
26
return new Promise((resolve, reject) => {
27
const blob = new Blob([data as Uint8Array<ArrayBuffer>], { type: mimeType });
28
const img = new Image();
29
const url = URL.createObjectURL(blob);
30
img.src = url;
31
32
img.onload = () => {
33
URL.revokeObjectURL(url);
34
let { width, height } = img;
35
36
if ((width <= 768 || height <= 768) && !isGif) {
37
resolve(data);
38
return;
39
}
40
41
// Calculate the new dimensions while maintaining the aspect ratio
42
if (width > 2048 || height > 2048) {
43
const scaleFactor = 2048 / Math.max(width, height);
44
width = Math.round(width * scaleFactor);
45
height = Math.round(height * scaleFactor);
46
}
47
48
const scaleFactor = 768 / Math.min(width, height);
49
width = Math.round(width * scaleFactor);
50
height = Math.round(height * scaleFactor);
51
52
const canvas = document.createElement('canvas');
53
canvas.width = width;
54
canvas.height = height;
55
const ctx = canvas.getContext('2d');
56
if (ctx) {
57
ctx.drawImage(img, 0, 0, width, height);
58
59
const jpegTypes = ['image/jpeg', 'image/jpg'];
60
const outputMimeType = mimeType && jpegTypes.includes(mimeType) ? 'image/jpeg' : 'image/png';
61
62
canvas.toBlob(blob => {
63
if (blob) {
64
const reader = new FileReader();
65
reader.onload = () => {
66
resolve(new Uint8Array(reader.result as ArrayBuffer));
67
};
68
reader.onerror = (error) => reject(error);
69
reader.readAsArrayBuffer(blob);
70
} else {
71
reject(new Error('Failed to create blob from canvas'));
72
}
73
}, outputMimeType);
74
} else {
75
reject(new Error('Failed to get canvas context'));
76
}
77
};
78
img.onerror = (error) => {
79
URL.revokeObjectURL(url);
80
reject(error);
81
};
82
});
83
}
84
85
export function convertStringToUInt8Array(data: string): Uint8Array {
86
const base64Data = data.includes(',') ? data.split(',')[1] : data;
87
if (isValidBase64(base64Data)) {
88
return decodeBase64(base64Data).buffer;
89
}
90
return new TextEncoder().encode(data);
91
}
92
93
// Only used for URLs
94
export function convertUint8ArrayToString(data: Uint8Array): string {
95
try {
96
const decoder = new TextDecoder();
97
const decodedString = decoder.decode(data);
98
return decodedString;
99
} catch {
100
return '';
101
}
102
}
103
104
function isValidBase64(str: string): boolean {
105
// checks if the string is a valid base64 string that is NOT encoded
106
return /^[A-Za-z0-9+/]*={0,2}$/.test(str) && (() => {
107
try {
108
atob(str);
109
return true;
110
} catch {
111
return false;
112
}
113
})();
114
}
115
116
export async function createFileForMedia(fileService: IFileService, imagesFolder: URI, dataTransfer: Uint8Array, mimeType: string): Promise<URI | undefined> {
117
const exists = await fileService.exists(imagesFolder);
118
if (!exists) {
119
await fileService.createFolder(imagesFolder);
120
}
121
122
const ext = mimeType.split('/')[1] || 'png';
123
const filename = `image-${Date.now()}.${ext}`;
124
const fileUri = joinPath(imagesFolder, filename);
125
126
const buffer = VSBuffer.wrap(dataTransfer);
127
await fileService.writeFile(fileUri, buffer);
128
129
return fileUri;
130
}
131
132
export async function cleanupOldImages(fileService: IFileService, logService: ILogService, imagesFolder: URI): Promise<void> {
133
const exists = await fileService.exists(imagesFolder);
134
if (!exists) {
135
return;
136
}
137
138
const duration = 7 * 24 * 60 * 60 * 1000; // 7 days
139
const files = await fileService.resolve(imagesFolder);
140
if (!files.children) {
141
return;
142
}
143
144
await Promise.all(files.children.map(async (file) => {
145
try {
146
const timestamp = getTimestampFromFilename(file.name);
147
if (timestamp && (Date.now() - timestamp > duration)) {
148
await fileService.del(file.resource);
149
}
150
} catch (err) {
151
logService.error('Failed to clean up old images', err);
152
}
153
}));
154
}
155
156
function getTimestampFromFilename(filename: string): number | undefined {
157
const match = filename.match(/image-(\d+)\./);
158
if (match) {
159
return parseInt(match[1], 10);
160
}
161
return undefined;
162
}
163
164