UNPKG

@raven-js/cortex

Version:

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

90 lines (77 loc) 3.39 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 PNG decoder - Pure function to decode PNG buffer to RGBA pixels. * * Extracts PNG decoding logic from PNGImage class into a pure function * that takes a PNG buffer and returns pixel data, dimensions, and metadata. */ import { decodeIHDR } from "./decode-ihdr.js"; import { decompressIDAT } from "./decompress-idat.js"; import { extractMetadata } from "./extract-metadata.js"; import { findChunksByType, parseChunks } from "./parse-chunks.js"; import { reconstructPixels } from "./reconstruct-pixels.js"; import { reverseFilters } from "./reverse-filters.js"; import { validatePNGSignature } from "./validate-signature.js"; /** * Decode PNG buffer to RGBA pixel data. * * @param {ArrayBuffer|Uint8Array} buffer - PNG file buffer * @returns {Promise<{pixels: Uint8Array, width: number, height: number, metadata: Object}>} Decoded image data * @throws {Error} If PNG decoding fails * * @example * const pngBuffer = readFileSync('image.png'); * const { pixels, width, height, metadata } = await decodePNG(pngBuffer); * console.log(`Decoded ${width}×${height} PNG with ${pixels.length} RGBA bytes`); */ export async function decodePNG(buffer) { // Convert to Uint8Array if needed const rawData = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer; try { // Step 1: Validate PNG signature if (!validatePNGSignature(rawData)) { throw new Error("Invalid PNG signature"); } // Step 2: Parse PNG chunks const chunkData = rawData.slice(8); // Skip 8-byte signature const chunks = parseChunks(chunkData); // Step 3: Decode IHDR chunk (must be first) const ihdrChunks = findChunksByType(chunks, "IHDR"); if (ihdrChunks.length !== 1) { throw new Error(`Expected exactly 1 IHDR chunk, found ${ihdrChunks.length}`); } const ihdr = decodeIHDR(/** @type {any} */ (ihdrChunks[0]).data); // Step 4: Decompress IDAT chunks const idatChunks = findChunksByType(chunks, "IDAT"); if (idatChunks.length === 0) { throw new Error("No IDAT chunks found"); } const idatData = idatChunks.map((chunk) => /** @type {any} */ (chunk).data); const compressedData = await decompressIDAT(idatData); // Step 5: Reverse PNG scanline filters const unfilteredData = reverseFilters(compressedData, ihdr.width, ihdr.height, ihdr.bytesPerPixel); // Step 6: Reconstruct RGBA pixels const pixels = reconstructPixels(unfilteredData, ihdr); // Step 7: Extract metadata const metadata = await extractMetadata(chunks); console.log(`✓ Successfully decoded PNG: ${ihdr.width}×${ihdr.height}, ${ihdr.channels} channels`); console.log(` - Bit depth: ${ihdr.bitDepth}, Color type: ${ihdr.colorType}`); console.log(` - Unfiltered data: ${unfilteredData.length} bytes`); console.log(` - Reconstructed pixels: ${pixels.length} bytes (${pixels.length / 4} RGBA pixels)`); console.log(` - Metadata: ${Object.keys(/** @type {any} */ (metadata).text || {}).length} text entries`); return { pixels, width: ihdr.width, height: ihdr.height, metadata, }; } catch (error) { throw new Error(`PNG decoding failed: ${error.message}`); } }