Path: blob/main/src/vs/platform/imageResize/browser/imageResizeService.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 '../../files/common/files.js';9import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';10import { ILogService } from '../../log/common/log.js';11import { IImageResizeService } from '../common/imageResizeService.js';121314export class ImageResizeService implements IImageResizeService {1516declare readonly _serviceBrand: undefined;1718/**19* Resizes an image provided as a UInt8Array string. Resizing is based on Open AI's algorithm for tokenzing images.20* https://platform.openai.com/docs/guides/vision#calculating-costs21* @param data - The UInt8Array string of the image to resize.22* @returns A promise that resolves to the UInt8Array string of the resized image.23*/2425async resizeImage(data: Uint8Array | string, mimeType?: string): Promise<Uint8Array> {26const isGif = mimeType === 'image/gif';2728if (typeof data === 'string') {29data = this.convertStringToUInt8Array(data);30}3132return new Promise((resolve, reject) => {33const blob = new Blob([data as Uint8Array<ArrayBuffer>], { type: mimeType });34const img = new Image();35const url = URL.createObjectURL(blob);36img.src = url;3738img.onload = () => {39URL.revokeObjectURL(url);40let { width, height } = img;4142if ((width <= 768 || height <= 768) && !isGif) {43resolve(data);44return;45}4647// Calculate the new dimensions while maintaining the aspect ratio48if (width > 2048 || height > 2048) {49const scaleFactor = 2048 / Math.max(width, height);50width = Math.round(width * scaleFactor);51height = Math.round(height * scaleFactor);52}5354const scaleFactor = 768 / Math.min(width, height);55width = Math.round(width * scaleFactor);56height = Math.round(height * scaleFactor);5758const canvas = document.createElement('canvas');59canvas.width = width;60canvas.height = height;61const ctx = canvas.getContext('2d');62if (ctx) {63ctx.drawImage(img, 0, 0, width, height);6465const jpegTypes = ['image/jpeg', 'image/jpg'];66const outputMimeType = mimeType && jpegTypes.includes(mimeType) ? 'image/jpeg' : 'image/png';6768canvas.toBlob(blob => {69if (blob) {70const reader = new FileReader();71reader.onload = () => {72resolve(new Uint8Array(reader.result as ArrayBuffer));73};74reader.onerror = (error) => reject(error);75reader.readAsArrayBuffer(blob);76} else {77reject(new Error('Failed to create blob from canvas'));78}79}, outputMimeType);80} else {81reject(new Error('Failed to get canvas context'));82}83};84img.onerror = (error) => {85URL.revokeObjectURL(url);86reject(error);87};88});89}9091convertStringToUInt8Array(data: string): Uint8Array {92const base64Data = data.includes(',') ? data.split(',')[1] : data;93if (this.isValidBase64(base64Data)) {94return decodeBase64(base64Data).buffer;95}96return new TextEncoder().encode(data);97}9899// Only used for URLs100convertUint8ArrayToString(data: Uint8Array): string {101try {102const decoder = new TextDecoder();103const decodedString = decoder.decode(data);104return decodedString;105} catch {106return '';107}108}109110isValidBase64(str: string): boolean {111try {112decodeBase64(str);113return true;114} catch {115return false;116}117}118119async createFileForMedia(fileService: IFileService, imagesFolder: URI, dataTransfer: Uint8Array, mimeType: string): Promise<URI | undefined> {120const exists = await fileService.exists(imagesFolder);121if (!exists) {122await fileService.createFolder(imagesFolder);123}124125const ext = mimeType.split('/')[1] || 'png';126const filename = `image-${Date.now()}.${ext}`;127const fileUri = joinPath(imagesFolder, filename);128129const buffer = VSBuffer.wrap(dataTransfer);130await fileService.writeFile(fileUri, buffer);131132return fileUri;133}134135async cleanupOldImages(fileService: IFileService, logService: ILogService, imagesFolder: URI): Promise<void> {136const exists = await fileService.exists(imagesFolder);137if (!exists) {138return;139}140141const duration = 7 * 24 * 60 * 60 * 1000; // 7 days142const files = await fileService.resolve(imagesFolder);143if (!files.children) {144return;145}146147await Promise.all(files.children.map(async (file) => {148try {149const timestamp = this.getTimestampFromFilename(file.name);150if (timestamp && (Date.now() - timestamp > duration)) {151await fileService.del(file.resource);152}153} catch (err) {154logService.error('Failed to clean up old images', err);155}156}));157}158159getTimestampFromFilename(filename: string): number | undefined {160const match = filename.match(/image-(\d+)\./);161if (match) {162return parseInt(match[1], 10);163}164return undefined;165}166167168}169170registerSingleton(IImageResizeService, ImageResizeService, InstantiationType.Delayed);171172173