Path: blob/main/src/vs/workbench/contrib/chat/browser/imageUtils.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { decodeBase64, VSBuffer } from '../../../../base/common/buffer.js';6import { joinPath } from '../../../../base/common/resources.js';7import { URI } from '../../../../base/common/uri.js';8import { IFileService } from '../../../../platform/files/common/files.js';9import { ILogService } from '../../../../platform/log/common/log.js';1011/**12* Resizes an image provided as a UInt8Array string. Resizing is based on Open AI's algorithm for tokenzing images.13* https://platform.openai.com/docs/guides/vision#calculating-costs14* @param data - The UInt8Array string of the image to resize.15* @returns A promise that resolves to the UInt8Array string of the resized image.16*/1718export async function resizeImage(data: Uint8Array | string, mimeType?: string): Promise<Uint8Array> {19const isGif = mimeType === 'image/gif';2021if (typeof data === 'string') {22data = convertStringToUInt8Array(data);23}2425return new Promise((resolve, reject) => {26const blob = new Blob([data as Uint8Array<ArrayBuffer>], { type: mimeType });27const img = new Image();28const url = URL.createObjectURL(blob);29img.src = url;3031img.onload = () => {32URL.revokeObjectURL(url);33let { width, height } = img;3435if ((width <= 768 || height <= 768) && !isGif) {36resolve(data);37return;38}3940// Calculate the new dimensions while maintaining the aspect ratio41if (width > 2048 || height > 2048) {42const scaleFactor = 2048 / Math.max(width, height);43width = Math.round(width * scaleFactor);44height = Math.round(height * scaleFactor);45}4647const scaleFactor = 768 / Math.min(width, height);48width = Math.round(width * scaleFactor);49height = Math.round(height * scaleFactor);5051const canvas = document.createElement('canvas');52canvas.width = width;53canvas.height = height;54const ctx = canvas.getContext('2d');55if (ctx) {56ctx.drawImage(img, 0, 0, width, height);5758const jpegTypes = ['image/jpeg', 'image/jpg'];59const outputMimeType = mimeType && jpegTypes.includes(mimeType) ? 'image/jpeg' : 'image/png';6061canvas.toBlob(blob => {62if (blob) {63const reader = new FileReader();64reader.onload = () => {65resolve(new Uint8Array(reader.result as ArrayBuffer));66};67reader.onerror = (error) => reject(error);68reader.readAsArrayBuffer(blob);69} else {70reject(new Error('Failed to create blob from canvas'));71}72}, outputMimeType);73} else {74reject(new Error('Failed to get canvas context'));75}76};77img.onerror = (error) => {78URL.revokeObjectURL(url);79reject(error);80};81});82}8384export function convertStringToUInt8Array(data: string): Uint8Array {85const base64Data = data.includes(',') ? data.split(',')[1] : data;86if (isValidBase64(base64Data)) {87return decodeBase64(base64Data).buffer;88}89return new TextEncoder().encode(data);90}9192// Only used for URLs93export function convertUint8ArrayToString(data: Uint8Array): string {94try {95const decoder = new TextDecoder();96const decodedString = decoder.decode(data);97return decodedString;98} catch {99return '';100}101}102103function isValidBase64(str: string): boolean {104// checks if the string is a valid base64 string that is NOT encoded105return /^[A-Za-z0-9+/]*={0,2}$/.test(str) && (() => {106try {107atob(str);108return true;109} catch {110return false;111}112})();113}114115export async function createFileForMedia(fileService: IFileService, imagesFolder: URI, dataTransfer: Uint8Array, mimeType: string): Promise<URI | undefined> {116const exists = await fileService.exists(imagesFolder);117if (!exists) {118await fileService.createFolder(imagesFolder);119}120121const ext = mimeType.split('/')[1] || 'png';122const filename = `image-${Date.now()}.${ext}`;123const fileUri = joinPath(imagesFolder, filename);124125const buffer = VSBuffer.wrap(dataTransfer);126await fileService.writeFile(fileUri, buffer);127128return fileUri;129}130131export async function cleanupOldImages(fileService: IFileService, logService: ILogService, imagesFolder: URI): Promise<void> {132const exists = await fileService.exists(imagesFolder);133if (!exists) {134return;135}136137const duration = 7 * 24 * 60 * 60 * 1000; // 7 days138const files = await fileService.resolve(imagesFolder);139if (!files.children) {140return;141}142143await Promise.all(files.children.map(async (file) => {144try {145const timestamp = getTimestampFromFilename(file.name);146if (timestamp && (Date.now() - timestamp > duration)) {147await fileService.del(file.resource);148}149} catch (err) {150logService.error('Failed to clean up old images', err);151}152}));153}154155function getTimestampFromFilename(filename: string): number | undefined {156const match = filename.match(/image-(\d+)\./);157if (match) {158return parseInt(match[1], 10);159}160return undefined;161}162163164