Files
nasir@endelospay.com d97cad1736 first commit
2025-08-12 02:54:17 +05:00

195 lines
6.2 KiB
TypeScript

/**
* Image processing utility functions
*/
/**
* Create ImageBitmap from data URL (for OffscreenCanvas)
* @param dataUrl Image data URL
* @returns Created ImageBitmap object
*/
export async function createImageBitmapFromUrl(dataUrl: string): Promise<ImageBitmap> {
const response = await fetch(dataUrl);
const blob = await response.blob();
return await createImageBitmap(blob);
}
/**
* Stitch multiple image parts (dataURL) onto a single canvas
* @param parts Array of image parts, each containing dataUrl and y coordinate
* @param totalWidthPx Total width (pixels)
* @param totalHeightPx Total height (pixels)
* @returns Stitched canvas
*/
export async function stitchImages(
parts: { dataUrl: string; y: number }[],
totalWidthPx: number,
totalHeightPx: number,
): Promise<OffscreenCanvas> {
const canvas = new OffscreenCanvas(totalWidthPx, totalHeightPx);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Unable to get canvas context');
}
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const part of parts) {
try {
const img = await createImageBitmapFromUrl(part.dataUrl);
const sx = 0;
const sy = 0;
const sWidth = img.width;
let sHeight = img.height;
const dy = part.y;
if (dy + sHeight > totalHeightPx) {
sHeight = totalHeightPx - dy;
}
if (sHeight <= 0) continue;
ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, dy, sWidth, sHeight);
} catch (error) {
console.error('Error stitching image part:', error, part);
}
}
return canvas;
}
/**
* Crop image (from dataURL) to specified rectangle and resize
* @param originalDataUrl Original image data URL
* @param cropRectPx Crop rectangle (physical pixels)
* @param dpr Device pixel ratio
* @param targetWidthOpt Optional target output width (CSS pixels)
* @param targetHeightOpt Optional target output height (CSS pixels)
* @returns Cropped canvas
*/
export async function cropAndResizeImage(
originalDataUrl: string,
cropRectPx: { x: number; y: number; width: number; height: number },
dpr: number = 1,
targetWidthOpt?: number,
targetHeightOpt?: number,
): Promise<OffscreenCanvas> {
const img = await createImageBitmapFromUrl(originalDataUrl);
let sx = cropRectPx.x;
let sy = cropRectPx.y;
let sWidth = cropRectPx.width;
let sHeight = cropRectPx.height;
// Ensure crop area is within image boundaries
if (sx < 0) {
sWidth += sx;
sx = 0;
}
if (sy < 0) {
sHeight += sy;
sy = 0;
}
if (sx + sWidth > img.width) {
sWidth = img.width - sx;
}
if (sy + sHeight > img.height) {
sHeight = img.height - sy;
}
if (sWidth <= 0 || sHeight <= 0) {
throw new Error(
'Invalid calculated crop size (<=0). Element may not be visible or fully captured.',
);
}
const finalCanvasWidthPx = targetWidthOpt ? targetWidthOpt * dpr : sWidth;
const finalCanvasHeightPx = targetHeightOpt ? targetHeightOpt * dpr : sHeight;
const canvas = new OffscreenCanvas(finalCanvasWidthPx, finalCanvasHeightPx);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Unable to get canvas context');
}
ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, finalCanvasWidthPx, finalCanvasHeightPx);
return canvas;
}
/**
* Convert canvas to data URL
* @param canvas Canvas
* @param format Image format
* @param quality JPEG quality (0-1)
* @returns Data URL
*/
export async function canvasToDataURL(
canvas: OffscreenCanvas,
format: string = 'image/png',
quality?: number,
): Promise<string> {
const blob = await canvas.convertToBlob({
type: format,
quality: format === 'image/jpeg' ? quality : undefined,
});
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
/**
* Compresses an image by scaling it and converting it to a target format with a specific quality.
* This is the most effective way to reduce image data size for transport or storage.
*
* @param {string} imageDataUrl - The original image data URL (e.g., from captureVisibleTab).
* @param {object} options - Compression options.
* @param {number} [options.scale=1.0] - The scaling factor for dimensions (e.g., 0.7 for 70%).
* @param {number} [options.quality=0.8] - The quality for lossy formats like JPEG (0.0 to 1.0).
* @param {string} [options.format='image/jpeg'] - The target image format.
* @returns {Promise<{dataUrl: string, mimeType: string}>} A promise that resolves to the compressed image data URL and its MIME type.
*/
export async function compressImage(
imageDataUrl: string,
options: { scale?: number; quality?: number; format?: 'image/jpeg' | 'image/webp' },
): Promise<{ dataUrl: string; mimeType: string }> {
const { scale = 1.0, quality = 0.8, format = 'image/jpeg' } = options;
// 1. Create an ImageBitmap from the original data URL for efficient drawing.
const imageBitmap = await createImageBitmapFromUrl(imageDataUrl);
// 2. Calculate the new dimensions based on the scale factor.
const newWidth = Math.round(imageBitmap.width * scale);
const newHeight = Math.round(imageBitmap.height * scale);
// 3. Use OffscreenCanvas for performance, as it doesn't need to be in the DOM.
const canvas = new OffscreenCanvas(newWidth, newHeight);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get 2D context from OffscreenCanvas');
}
// 4. Draw the original image onto the smaller canvas, effectively resizing it.
ctx.drawImage(imageBitmap, 0, 0, newWidth, newHeight);
// 5. Export the canvas content to the target format with the specified quality.
// This is the step that performs the data compression.
const compressedDataUrl = await canvas.convertToBlob({ type: format, quality: quality });
// A helper to convert blob to data URL since OffscreenCanvas.toDataURL is not standard yet
// on all execution contexts (like service workers).
const dataUrl = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(compressedDataUrl);
});
return { dataUrl, mimeType: format };
}