import { createModuleLogger } from "./LoggerUtils.js"; import { createCanvas } from "./CommonUtils.js"; import { convertToImage } from "./ImageUtils.js"; import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; const log = createModuleLogger('MaskProcessingUtils'); /** * Utility functions for processing masks and image data */ export interface MaskProcessingOptions { /** Target width for the processed mask */ targetWidth?: number; /** Target height for the processed mask */ targetHeight?: number; /** Whether to invert the alpha channel (default: true) */ invertAlpha?: boolean; /** Mask color RGB values (default: {r: 255, g: 255, b: 255}) */ maskColor?: { r: number; g: number; b: number }; } /** * Processes an image to create a mask with inverted alpha channel * @param sourceImage - Source image or canvas element * @param options - Processing options * @returns Promise with processed mask as HTMLCanvasElement */ export const processImageToMask = withErrorHandling(async function( sourceImage: HTMLImageElement | HTMLCanvasElement, options: MaskProcessingOptions = {} ): Promise { if (!sourceImage) { throw createValidationError("Source image is required", { sourceImage }); } const { targetWidth = sourceImage.width, targetHeight = sourceImage.height, invertAlpha = true, maskColor = { r: 255, g: 255, b: 255 } } = options; log.debug('Processing image to mask:', { sourceSize: { width: sourceImage.width, height: sourceImage.height }, targetSize: { width: targetWidth, height: targetHeight }, invertAlpha, maskColor }); // Create temporary canvas for processing const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true }); if (!tempCtx) { throw createValidationError("Failed to get 2D context for mask processing"); } // Draw the source image tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight); // Get image data for processing const imageData = tempCtx.getImageData(0, 0, targetWidth, targetHeight); const data = imageData.data; // Process pixels to create mask for (let i = 0; i < data.length; i += 4) { const originalAlpha = data[i + 3]; // Set RGB to mask color data[i] = maskColor.r; // Red data[i + 1] = maskColor.g; // Green data[i + 2] = maskColor.b; // Blue // Handle alpha channel if (invertAlpha) { data[i + 3] = 255 - originalAlpha; // Invert alpha } else { data[i + 3] = originalAlpha; // Keep original alpha } } // Put processed data back to canvas tempCtx.putImageData(imageData, 0, 0); log.debug('Mask processing completed'); return tempCanvas; }, 'processImageToMask'); /** * Processes image data with custom pixel transformation * @param sourceImage - Source image or canvas element * @param pixelTransform - Custom pixel transformation function * @param options - Processing options * @returns Promise with processed image as HTMLCanvasElement */ export const processImageWithTransform = withErrorHandling(async function( sourceImage: HTMLImageElement | HTMLCanvasElement, pixelTransform: (r: number, g: number, b: number, a: number, index: number) => [number, number, number, number], options: MaskProcessingOptions = {} ): Promise { if (!sourceImage) { throw createValidationError("Source image is required", { sourceImage }); } if (!pixelTransform || typeof pixelTransform !== 'function') { throw createValidationError("Pixel transform function is required", { pixelTransform }); } const { targetWidth = sourceImage.width, targetHeight = sourceImage.height } = options; const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true }); if (!tempCtx) { throw createValidationError("Failed to get 2D context for image processing"); } tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight); const imageData = tempCtx.getImageData(0, 0, targetWidth, targetHeight); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const [r, g, b, a] = pixelTransform(data[i], data[i + 1], data[i + 2], data[i + 3], i / 4); data[i] = r; data[i + 1] = g; data[i + 2] = b; data[i + 3] = a; } tempCtx.putImageData(imageData, 0, 0); return tempCanvas; }, 'processImageWithTransform'); /** * Crops an image to a specific region * @param sourceImage - Source image or canvas * @param cropArea - Crop area {x, y, width, height} * @returns Promise with cropped image as HTMLCanvasElement */ export const cropImage = withErrorHandling(async function( sourceImage: HTMLImageElement | HTMLCanvasElement, cropArea: { x: number; y: number; width: number; height: number } ): Promise { if (!sourceImage) { throw createValidationError("Source image is required", { sourceImage }); } if (!cropArea || typeof cropArea !== 'object') { throw createValidationError("Crop area is required", { cropArea }); } const { x, y, width, height } = cropArea; if (width <= 0 || height <= 0) { throw createValidationError("Crop area must have positive width and height", { cropArea }); } log.debug('Cropping image:', { sourceSize: { width: sourceImage.width, height: sourceImage.height }, cropArea }); const { canvas, ctx } = createCanvas(width, height); if (!ctx) { throw createValidationError("Failed to get 2D context for image cropping"); } ctx.drawImage( sourceImage, x, y, width, height, // Source rectangle 0, 0, width, height // Destination rectangle ); return canvas; }, 'cropImage'); /** * Applies a mask to an image using viewport positioning * @param maskImage - Mask image or canvas * @param targetWidth - Target viewport width * @param targetHeight - Target viewport height * @param viewportOffset - Viewport offset {x, y} * @param maskColor - Mask color (default: white) * @returns Promise with processed mask for viewport */ export const processMaskForViewport = withErrorHandling(async function( maskImage: HTMLImageElement | HTMLCanvasElement, targetWidth: number, targetHeight: number, viewportOffset: { x: number; y: number }, maskColor: { r: number; g: number; b: number } = { r: 255, g: 255, b: 255 } ): Promise { if (!maskImage) { throw createValidationError("Mask image is required", { maskImage }); } if (!viewportOffset || typeof viewportOffset !== 'object') { throw createValidationError("Viewport offset is required", { viewportOffset }); } if (targetWidth <= 0 || targetHeight <= 0) { throw createValidationError("Target dimensions must be positive", { targetWidth, targetHeight }); } log.debug("Processing mask for viewport:", { sourceSize: { width: maskImage.width, height: maskImage.height }, targetSize: { width: targetWidth, height: targetHeight }, viewportOffset }); const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true }); if (!tempCtx) { throw createValidationError("Failed to get 2D context for viewport mask processing"); } // Calculate source coordinates based on viewport offset const sourceX = -viewportOffset.x; const sourceY = -viewportOffset.y; // Draw the mask with viewport cropping tempCtx.drawImage( maskImage, // Source: full mask from "output area" sourceX, // sx: Real X coordinate on large mask sourceY, // sy: Real Y coordinate on large mask targetWidth, // sWidth: Width of cropped fragment targetHeight, // sHeight: Height of cropped fragment 0, // dx: Where to paste in target canvas (always 0) 0, // dy: Where to paste in target canvas (always 0) targetWidth, // dWidth: Width of pasted image targetHeight // dHeight: Height of pasted image ); // Apply mask color const imageData = tempCtx.getImageData(0, 0, targetWidth, targetHeight); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const alpha = data[i + 3]; if (alpha > 0) { data[i] = maskColor.r; data[i + 1] = maskColor.g; data[i + 2] = maskColor.b; } } tempCtx.putImageData(imageData, 0, 0); log.debug("Viewport mask processing completed"); return tempCanvas; }, 'processMaskForViewport');