UNPKG

@raven-js/cortex

Version:

Zero-dependency machine learning, AI, and data processing library for modern JavaScript

306 lines (283 loc) 9.42 kB
/** * @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 Decodes and validates PNG IHDR (Image Header) chunks. * * The IHDR chunk contains critical image parameters that define the PNG canvas: * - Image dimensions (width, height) * - Bit depth per sample * - Color type (grayscale, RGB, palette, etc.) * - Compression method (always 0 for DEFLATE) * - Filter method (always 0 for adaptive filtering) * - Interlace method (0=none, 1=Adam7) * * This decoder validates all parameters against PNG specification limits * and provides structured access to image properties. * * @param {Uint8Array} data - IHDR chunk data (13 bytes) * @returns {{width: number, height: number, bitDepth: number, colorType: number, compressionMethod: number, filterMethod: number, interlaceMethod: number, channels: number, hasAlpha: boolean, bytesPerPixel: number, samplesPerPixel: number}} Decoded IHDR information * * @example * // Decode IHDR from chunk data * const ihdr = decodeIHDR(ihdrChunk.data); * console.log(`${ihdr.width}x${ihdr.height}, ${ihdr.bitDepth}-bit ${getColorTypeName(ihdr.colorType)}`); * * @example * // Check for alpha channel * if (ihdr.hasAlpha) { * console.log('Image has transparency'); * } */ /** * PNG color type constants as defined in PNG specification. * Each color type determines the interpretation of pixel data. */ export const COLOR_TYPES = { /** Grayscale: Each pixel is a grayscale sample */ GRAYSCALE: 0, /** RGB: Each pixel is an RGB triple */ RGB: 2, /** Palette: Each pixel is a palette index */ PALETTE: 3, /** Grayscale + Alpha: Each pixel is a grayscale sample followed by alpha */ GRAYSCALE_ALPHA: 4, /** RGB + Alpha: Each pixel is an RGB triple followed by alpha */ RGB_ALPHA: 6, }; /** * Valid bit depths for each color type according to PNG specification. * Enforces PNG's strict bit depth requirements per color type. */ export const VALID_BIT_DEPTHS = { [COLOR_TYPES.GRAYSCALE]: [1, 2, 4, 8, 16], [COLOR_TYPES.RGB]: [8, 16], [COLOR_TYPES.PALETTE]: [1, 2, 4, 8], [COLOR_TYPES.GRAYSCALE_ALPHA]: [8, 16], [COLOR_TYPES.RGB_ALPHA]: [8, 16], }; /** * Read 32-bit big-endian unsigned integer from buffer. * * @param {Uint8Array} buffer - Buffer to read from * @param {number} offset - Byte offset to read from * @returns {number} 32-bit unsigned integer * * @private */ function readUint32BE(buffer, offset) { return ((buffer[offset] << 24) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | buffer[offset + 3]) >>> 0; } /** * Calculate number of channels for a color type. * Channels represent independent color/alpha components. * * @param {number} colorType - PNG color type * @returns {number} Number of channels (1-4) * * @private */ function getChannelCount(colorType) { switch (colorType) { case COLOR_TYPES.GRAYSCALE: case COLOR_TYPES.PALETTE: return 1; case COLOR_TYPES.GRAYSCALE_ALPHA: return 2; case COLOR_TYPES.RGB: return 3; case COLOR_TYPES.RGB_ALPHA: return 4; default: throw new Error(`Invalid color type: ${colorType}`); } } /** * Calculate samples per pixel for a color type. * Samples are the actual data values stored per pixel. * For palette images, this is 1 (the index), regardless of final color channels. * * @param {number} colorType - PNG color type * @returns {number} Number of samples per pixel (1-4) * * @private */ function getSamplesPerPixel(colorType) { switch (colorType) { case COLOR_TYPES.GRAYSCALE: case COLOR_TYPES.PALETTE: return 1; case COLOR_TYPES.GRAYSCALE_ALPHA: return 2; case COLOR_TYPES.RGB: return 3; case COLOR_TYPES.RGB_ALPHA: return 4; default: throw new Error(`Invalid color type: ${colorType}`); } } /** * Check if color type includes alpha channel. * * @param {number} colorType - PNG color type * @returns {boolean} True if color type includes alpha * * @private */ function hasAlphaChannel(colorType) { return colorType === COLOR_TYPES.GRAYSCALE_ALPHA || colorType === COLOR_TYPES.RGB_ALPHA; } /** * Calculate bytes per pixel for given color type and bit depth. * This is the storage requirement per pixel in the decoded image data. * * @param {number} colorType - PNG color type * @param {number} bitDepth - Bit depth per sample * @returns {number} Bytes per pixel (may be fractional for sub-byte depths) * * @private */ function getBytesPerPixel(colorType, bitDepth) { const samplesPerPixel = getSamplesPerPixel(colorType); return (samplesPerPixel * bitDepth) / 8; } /** * Get human-readable name for color type. * Useful for debugging and error messages. * * @param {number} colorType - PNG color type * @returns {string} Color type name * * @example * console.log(getColorTypeName(2)); // "RGB" * console.log(getColorTypeName(6)); // "RGB+Alpha" */ export function getColorTypeName(colorType) { switch (colorType) { case COLOR_TYPES.GRAYSCALE: return "Grayscale"; case COLOR_TYPES.RGB: return "RGB"; case COLOR_TYPES.PALETTE: return "Palette"; case COLOR_TYPES.GRAYSCALE_ALPHA: return "Grayscale+Alpha"; case COLOR_TYPES.RGB_ALPHA: return "RGB+Alpha"; default: return `Unknown(${colorType})`; } } /** * Validate IHDR parameters against PNG specification. * Ensures all values are within valid ranges and combinations. * * @param {number} width - Image width in pixels * @param {number} height - Image height in pixels * @param {number} bitDepth - Bit depth per sample * @param {number} colorType - Color type * @param {number} compressionMethod - Compression method * @param {number} filterMethod - Filter method * @param {number} interlaceMethod - Interlace method * @throws {Error} If any parameter is invalid * * @private */ function validateIHDRParameters(width, height, bitDepth, colorType, compressionMethod, filterMethod, interlaceMethod) { // Validate dimensions if (width === 0 || width > 0x7fffffff) { throw new Error(`Invalid width: ${width} (must be 1-2147483647)`); } if (height === 0 || height > 0x7fffffff) { throw new Error(`Invalid height: ${height} (must be 1-2147483647)`); } // Validate color type if (!(colorType in VALID_BIT_DEPTHS)) { throw new Error(`Invalid color type: ${colorType}`); } // Validate bit depth for color type const validDepths = VALID_BIT_DEPTHS[colorType]; if (!validDepths.includes(bitDepth)) { throw new Error(`Invalid bit depth ${bitDepth} for color type ${getColorTypeName(colorType)} (valid: ${validDepths.join(", ")})`); } // Validate compression method (must be 0) if (compressionMethod !== 0) { throw new Error(`Invalid compression method: ${compressionMethod} (must be 0)`); } // Validate filter method (must be 0) if (filterMethod !== 0) { throw new Error(`Invalid filter method: ${filterMethod} (must be 0)`); } // Validate interlace method (0 or 1) if (interlaceMethod !== 0 && interlaceMethod !== 1) { throw new Error(`Invalid interlace method: ${interlaceMethod} (must be 0 or 1)`); } } /** * Decode PNG IHDR (Image Header) chunk. * * Parses the 13-byte IHDR chunk data and extracts image parameters. * Validates all parameters against PNG specification requirements. * Returns structured object with image properties and derived values. * * @param {Uint8Array} data - IHDR chunk data (must be exactly 13 bytes) * @returns {{width: number, height: number, bitDepth: number, colorType: number, compressionMethod: number, filterMethod: number, interlaceMethod: number, channels: number, hasAlpha: boolean, bytesPerPixel: number, samplesPerPixel: number}} Decoded IHDR information * @throws {Error} If data is invalid or parameters violate PNG specification * * @example * // Decode IHDR from parsed chunk * const ihdrChunk = findChunksByType(chunks, 'IHDR')[0]; * const ihdr = decodeIHDR(ihdrChunk.data); * console.log(`Image: ${ihdr.width}x${ihdr.height}, ${ihdr.bitDepth}-bit`); * * @example * // Check image properties * const ihdr = decodeIHDR(data); * if (ihdr.interlaceMethod === 1) { * console.log('Image uses Adam7 interlacing'); * } * if (ihdr.colorType === COLOR_TYPES.PALETTE) { * console.log('Palette-based image, need PLTE chunk'); * } */ export function decodeIHDR(data) { if (!(data instanceof Uint8Array)) { throw new TypeError("Expected data to be Uint8Array"); } if (data.length !== 13) { throw new Error(`IHDR chunk must be exactly 13 bytes, got ${data.length}`); } // Parse IHDR fields (PNG spec section 11.2.2) const width = readUint32BE(data, 0); const height = readUint32BE(data, 4); const bitDepth = data[8]; const colorType = data[9]; const compressionMethod = data[10]; const filterMethod = data[11]; const interlaceMethod = data[12]; // Validate all parameters validateIHDRParameters(width, height, bitDepth, colorType, compressionMethod, filterMethod, interlaceMethod); // Calculate derived properties const channels = getChannelCount(colorType); const hasAlpha = hasAlphaChannel(colorType); const bytesPerPixel = getBytesPerPixel(colorType, bitDepth); const samplesPerPixel = getSamplesPerPixel(colorType); return { width, height, bitDepth, colorType, compressionMethod, filterMethod, interlaceMethod, channels, hasAlpha, bytesPerPixel, samplesPerPixel, }; }