import { RefObject } from 'react'; import { bgColor, canvasSize } from './generativeFillConstants'; export interface APISuccess { status: 'success'; urls: string[]; } export interface APIError { status: 'error'; message: string; } export class ImageUtility { /** * * @param canvas Canvas to convert * @returns Blob of canvas */ static canvasToBlob = (canvas: HTMLCanvasElement): Promise => { return new Promise(resolve => { canvas.toBlob(blob => { if (blob) { resolve(blob); } }, 'image/png'); }); }; // given a square api image, get the cropped img static getCroppedImg = (img: HTMLImageElement, width: number, height: number): HTMLCanvasElement | undefined => { // Create a new canvas element const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (ctx) { // Clear the canvas ctx.clearRect(0, 0, canvas.width, canvas.height); if (width < height) { // horizontal padding, x offset const xOffset = (canvasSize - width) / 2; ctx.drawImage(img, xOffset, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); } else { // vertical padding, y offset const yOffset = (canvasSize - height) / 2; ctx.drawImage(img, 0, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); } return canvas; } }; // converts an image to a canvas data url static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise => { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const canvas = this.getCroppedImg(img, width, height); if (canvas) { const dataUrl = canvas.toDataURL(); resolve(dataUrl); } }; img.onerror = error => { reject(error); }; img.src = imageSrc; }); }; // calls the openai api to get image edits static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise => { const apiUrl = 'https://api.openai.com/v1/images/edits'; const fd = new FormData(); fd.append('image', imgBlob, 'image.png'); fd.append('mask', maskBlob, 'mask.png'); fd.append('prompt', prompt); fd.append('size', '1024x1024'); fd.append('n', n ? JSON.stringify(n) : '1'); fd.append('response_format', 'b64_json'); try { const res = await fetch(apiUrl, { method: 'POST', headers: { Authorization: `Bearer ${process.env.OPENAI_KEY}`, }, body: fd, }); const data = await res.json(); console.log(data.data); return { status: 'success', urls: (data.data as { b64_json: string }[]).map(data => `data:image/png;base64,${data.b64_json}`), }; } catch (err) { console.log(err); return { status: 'error', message: 'API error.' }; } }; // mock api call static mockGetEdit = async (mockSrc: string): Promise => { return { status: 'success', urls: [mockSrc, mockSrc, mockSrc], }; }; // Gets the canvas rendering context of a canvas static getCanvasContext = (canvasRef: RefObject): CanvasRenderingContext2D | null => { if (!canvasRef.current) return null; const ctx = canvasRef.current.getContext('2d'); if (!ctx) return null; return ctx; }; // Helper for downloading the canvas (for debugging) static downloadCanvas = (canvas: HTMLCanvasElement) => { const url = canvas.toDataURL(); const downloadLink = document.createElement('a'); downloadLink.href = url; downloadLink.download = 'canvas'; downloadLink.click(); downloadLink.remove(); }; // Download the canvas (for debugging) static downloadImageCanvas = (imgUrl: string) => { const img = new Image(); img.src = imgUrl; img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = canvasSize; canvas.height = canvasSize; const ctx = canvas.getContext('2d'); ctx?.drawImage(img, 0, 0, canvasSize, canvasSize); this.downloadCanvas(canvas); }; }; // Clears the canvas static clearCanvas = (canvasRef: React.RefObject) => { const ctx = this.getCanvasContext(canvasRef); if (!ctx || !canvasRef.current) return; ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); }; // Draws the image to the current canvas static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject, width: number, height: number) => { const drawImg = (img: HTMLImageElement) => { const ctx = this.getCanvasContext(canvasRef); if (!ctx) return; ctx.globalCompositeOperation = 'source-over'; ctx.clearRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height); }; if (img.complete) { drawImg(img); } else { img.onload = () => { drawImg(img); }; } }; // Gets the image mask for the openai endpoint static getCanvasMask = (srcCanvas: HTMLCanvasElement, paddedCanvas: HTMLCanvasElement): HTMLCanvasElement | undefined => { const canvas = document.createElement('canvas'); canvas.width = canvasSize; canvas.height = canvasSize; const ctx = canvas.getContext('2d'); if (!ctx) return; ctx?.clearRect(0, 0, canvasSize, canvasSize); ctx.drawImage(paddedCanvas, 0, 0); // extract and set padding data if (srcCanvas.height > srcCanvas.width) { // horizontal padding, x offset const xOffset = (canvasSize - srcCanvas.width) / 2; ctx?.clearRect(xOffset, 0, srcCanvas.width, srcCanvas.height); ctx.drawImage(srcCanvas, xOffset, 0, srcCanvas.width, srcCanvas.height); } else { // vertical padding, y offset const yOffset = (canvasSize - srcCanvas.height) / 2; ctx?.clearRect(0, yOffset, srcCanvas.width, srcCanvas.height); ctx.drawImage(srcCanvas, 0, yOffset, srcCanvas.width, srcCanvas.height); } return canvas; }; // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) static drawHorizontalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, xOffset: number) => { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; for (let i = 0; i < canvas.height; i++) { for (let j = 0; j < xOffset; j++) { const targetIdx = 4 * (i * canvas.width + j); const sourceI = i; const sourceJ = xOffset + (xOffset - j); const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); data[targetIdx] = data[sourceIdx]; data[targetIdx + 1] = data[sourceIdx + 1]; data[targetIdx + 2] = data[sourceIdx + 2]; } } for (let i = 0; i < canvas.height; i++) { for (let j = canvas.width - 1; j >= canvas.width - 1 - xOffset; j--) { const targetIdx = 4 * (i * canvas.width + j); const sourceI = i; const sourceJ = canvas.width - 1 - xOffset - (xOffset - (canvas.width - j)); const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); data[targetIdx] = data[sourceIdx]; data[targetIdx + 1] = data[sourceIdx + 1]; data[targetIdx + 2] = data[sourceIdx + 2]; } } ctx.putImageData(imageData, 0, 0); }; // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas) static drawVerticalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, yOffset: number) => { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; for (let j = 0; j < canvas.width; j++) { for (let i = 0; i < yOffset; i++) { const targetIdx = 4 * (i * canvas.width + j); const sourceJ = j; const sourceI = yOffset + (yOffset - i); const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); data[targetIdx] = data[sourceIdx]; data[targetIdx + 1] = data[sourceIdx + 1]; data[targetIdx + 2] = data[sourceIdx + 2]; } } for (let j = 0; j < canvas.width; j++) { for (let i = canvas.height - 1; i >= canvas.height - 1 - yOffset; i--) { const targetIdx = 4 * (i * canvas.width + j); const sourceJ = j; const sourceI = canvas.height - 1 - yOffset - (yOffset - (canvas.height - i)); const sourceIdx = 4 * (sourceI * canvas.width + sourceJ); data[targetIdx] = data[sourceIdx]; data[targetIdx + 1] = data[sourceIdx + 1]; data[targetIdx + 2] = data[sourceIdx + 2]; } } ctx.putImageData(imageData, 0, 0); }; // Gets the unaltered (besides filling in padding) version of the image for the api call static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement | undefined => { const canvas = document.createElement('canvas'); canvas.width = canvasSize; canvas.height = canvasSize; const ctx = canvas.getContext('2d'); if (!ctx) return; // fix scaling const scale = Math.min(canvasSize / img.width, canvasSize / img.height); const width = Math.floor(img.width * scale); const height = Math.floor(img.height * scale); ctx?.clearRect(0, 0, canvasSize, canvasSize); ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvasSize, canvasSize); // extract and set padding data if (img.naturalHeight > img.naturalWidth) { // horizontal padding, x offset const xOffset = Math.floor((canvasSize - width) / 2); ctx.drawImage(img, xOffset, 0, width, height); // draw reflected image padding this.drawHorizontalReflection(ctx, canvas, xOffset); } else { // vertical padding, y offset const yOffset = Math.floor((canvasSize - height) / 2); ctx.drawImage(img, 0, yOffset, width, height); // draw reflected image padding this.drawVerticalReflection(ctx, canvas, yOffset); } return canvas; }; }