@raven-js/cortex
Version:
Zero-dependency machine learning, AI, and data processing library for modern JavaScript
193 lines (173 loc) • 6.86 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* @file Utility functions for image cropping operations.
*
* This module provides validation, bounds checking, and helper functions
* for cropping RGBA pixel data. All functions are designed to be pure
* and testable in isolation.
*
* @example
* // Validate crop parameters
* validateCropParameters(pixels, 800, 600, 100, 50, 200, 150);
*
* // Clamp crop region to image bounds
* const bounds = clampCropRegion(800, 600, -10, -5, 220, 160);
* console.log(bounds); // { x: 0, y: 0, width: 210, height: 155 }
*/
/**
* Validates crop parameters for correctness and safety.
*
* @param {Uint8Array} pixels - Source RGBA pixel data
* @param {number} srcWidth - Source image width in pixels
* @param {number} srcHeight - Source image height in pixels
* @param {number} x - Crop region X coordinate (top-left)
* @param {number} y - Crop region Y coordinate (top-left)
* @param {number} width - Crop region width in pixels
* @param {number} height - Crop region height in pixels
* @throws {Error} If any parameter is invalid
*/
export function validateCropParameters(pixels, srcWidth, srcHeight, x, y, width, height) {
// Validate pixel data
if (!(pixels instanceof Uint8Array)) {
throw new Error("Pixels must be a Uint8Array");
}
// Validate source dimensions
if (!Number.isInteger(srcWidth) || srcWidth <= 0) {
throw new Error(`Invalid source width: ${srcWidth}. Must be positive integer`);
}
if (!Number.isInteger(srcHeight) || srcHeight <= 0) {
throw new Error(`Invalid source height: ${srcHeight}. Must be positive integer`);
}
// Validate pixel data size
const expectedSize = srcWidth * srcHeight * 4; // RGBA = 4 bytes per pixel
if (pixels.length !== expectedSize) {
throw new Error(`Invalid pixel data size: expected ${expectedSize}, got ${pixels.length}`);
}
// Validate crop coordinates
if (!Number.isInteger(x)) {
throw new Error(`Invalid crop X coordinate: ${x}. Must be integer`);
}
if (!Number.isInteger(y)) {
throw new Error(`Invalid crop Y coordinate: ${y}. Must be integer`);
}
// Validate crop dimensions
if (!Number.isInteger(width) || width <= 0) {
throw new Error(`Invalid crop width: ${width}. Must be positive integer`);
}
if (!Number.isInteger(height) || height <= 0) {
throw new Error(`Invalid crop height: ${height}. Must be positive integer`);
}
}
/**
* Clamps crop region to fit within image bounds.
* Adjusts coordinates and dimensions to ensure the crop region
* is entirely within the source image boundaries.
*
* @param {number} srcWidth - Source image width in pixels
* @param {number} srcHeight - Source image height in pixels
* @param {number} x - Crop region X coordinate (top-left)
* @param {number} y - Crop region Y coordinate (top-left)
* @param {number} width - Crop region width in pixels
* @param {number} height - Crop region height in pixels
* @returns {{x: number, y: number, width: number, height: number}} Clamped crop bounds
*/
export function clampCropRegion(srcWidth, srcHeight, x, y, width, height) {
// Clamp starting coordinates to image bounds
const clampedX = Math.max(0, Math.min(x, srcWidth - 1));
const clampedY = Math.max(0, Math.min(y, srcHeight - 1));
// Calculate maximum available dimensions from clamped coordinates
const maxWidth = srcWidth - clampedX;
const maxHeight = srcHeight - clampedY;
// Clamp dimensions to available space
const clampedWidth = Math.max(1, Math.min(width, maxWidth));
const clampedHeight = Math.max(1, Math.min(height, maxHeight));
return {
x: clampedX,
y: clampedY,
width: clampedWidth,
height: clampedHeight,
};
}
/**
* Calculates effective crop bounds, handling edge cases.
* Returns null if the crop region is entirely outside the image.
*
* @param {number} srcWidth - Source image width in pixels
* @param {number} srcHeight - Source image height in pixels
* @param {number} x - Crop region X coordinate (top-left)
* @param {number} y - Crop region Y coordinate (top-left)
* @param {number} width - Crop region width in pixels
* @param {number} height - Crop region height in pixels
* @returns {{x: number, y: number, width: number, height: number} | null} Effective crop bounds or null
*/
export function calculateCropBounds(srcWidth, srcHeight, x, y, width, height) {
// Check if crop region is entirely outside image bounds
if (x >= srcWidth || y >= srcHeight || x + width <= 0 || y + height <= 0) {
return null; // No intersection with image
}
// Calculate intersection with image bounds
const left = Math.max(0, x);
const top = Math.max(0, y);
const right = Math.min(srcWidth, x + width);
const bottom = Math.min(srcHeight, y + height);
// Calculate effective dimensions
const effectiveWidth = right - left;
const effectiveHeight = bottom - top;
// Ensure we have a valid region
if (effectiveWidth <= 0 || effectiveHeight <= 0) {
return null;
}
return {
x: left,
y: top,
width: effectiveWidth,
height: effectiveHeight,
};
}
/**
* Checks if a crop operation would be a no-op (identity crop).
* An identity crop covers the entire source image.
*
* @param {number} srcWidth - Source image width in pixels
* @param {number} srcHeight - Source image height in pixels
* @param {number} x - Crop region X coordinate (top-left)
* @param {number} y - Crop region Y coordinate (top-left)
* @param {number} width - Crop region width in pixels
* @param {number} height - Crop region height in pixels
* @returns {boolean} True if this is an identity crop
*/
export function isIdentityCrop(srcWidth, srcHeight, x, y, width, height) {
return x === 0 && y === 0 && width === srcWidth && height === srcHeight;
}
/**
* Gets the pixel index for RGBA data at given coordinates.
* Each pixel is 4 bytes (R, G, B, A) in row-major order.
*
* @param {number} x - X coordinate (column)
* @param {number} y - Y coordinate (row)
* @param {number} width - Image width in pixels
* @returns {number} Byte index in RGBA array
*/
export function getPixelIndex(x, y, width) {
return (y * width + x) * 4;
}
/**
* Copies a single RGBA pixel from source to destination array.
*
* @param {Uint8Array} src - Source RGBA pixel data
* @param {Uint8Array} dst - Destination RGBA pixel data
* @param {number} srcIndex - Source pixel byte index
* @param {number} dstIndex - Destination pixel byte index
*/
export function copyPixel(src, dst, srcIndex, dstIndex) {
dst[dstIndex] = src[srcIndex]; // Red
dst[dstIndex + 1] = src[srcIndex + 1]; // Green
dst[dstIndex + 2] = src[srcIndex + 2]; // Blue
dst[dstIndex + 3] = src[srcIndex + 3]; // Alpha
}