Source: lib/png-chunks-extractor.js

/**
 * @module lib/png-chunks-extractor
 * @requires module:lib/crc
 * @requires module:lib/png-struct
 */

import {readFile} from 'node:fs/promises';
import {validatePNGSignature, validateIHDR, validateIEND} from './png-struct.js';
import crcValidation from './crc.js';

function handleValidation(errCb, validationError) {
    if (validationError) {
        if (errCb) errCb(validationError);
        return validationError;
    }
    return null;
}

function handleErr(logOnError, throwOnError, err) {
    if (err) {
        if (logOnError) console.log(err);
        if (throwOnError) throw new Error(err);
    }
}

/**
 * @typedef {Object} Chunk
 * @property {Buffer} dataBuf - Chunk raw data
 * @property {number} type - uint 32, png chunk type
 * @global
 */

/**
 * Extract a chunk from a PNG buffer
 * @param {Buffer} buffer - Raw PNG Buffer
 * @param {number} offset - Current read offset
 * @param {boolean} [parseCrc=false] - If truthy, crc of each chunk is parsed
 * @returns {Object.<{offset: number, crc: number, crcBuf: Buffer, chunk: Chunk}>} Parsed chunk, chunk's CRC and new offset
 */
function extractNextChunk(buffer, offset, parseCrc = false) {
    const length = buffer.readUInt32BE(offset);
    offset += 4;
    const type = buffer.readUInt32BE(offset);
    const crcBuf = (parseCrc) ? buffer.subarray(offset, offset + 4 + length) : null;
    offset += 4;
    const dataBuf = buffer.subarray(offset, offset += length);
    const crc = (parseCrc) ? buffer.readInt32BE(offset) : 0;
    offset += 4;
    return {
        chunk: {
            dataBuf,
            type
        },
        offset,
        crc,
        crcBuf
    };
}

/**
 * Parse a PNG file and return its data chunks
 * @function default
 * @static
 * @param {PathLike|FileHandle} file - PNG file
 * @param {Object} [options] - Parsing options
 * @param {boolean} [options.validateStruct=true] - If truthy, validate the general PNG structure (file starts with PNG signature and chunk stream begins with an IHDR chunk and ends with an empty IEND chunk). Return null when validation fails
 * @param {boolean} [options.validateCrc=false] - If truthy, check crc signature of each chunk and ignore it if there is a mismatch, further mismatch behaviour depends on {@link options.throwOnError} and {@link options.logOnError}
 * @param {boolean} [options.throwOnError=false] - If truthy, throws error when validation fails
 * @param {boolean} [options.logOnError=true] - If truthy, logs if validation fails
 * @return {Promise.<{chunks: Array.<Chunk>, chunksByType: Array.<{type:number, chunks:Array.<Chunk>}> }> | null} The raw chunks array (by order of appearance) and an object of these chunks indexed by chunk type (as a 32 bit unsigned int)
 */
export default async function(file, options = {}) {
    const {
        validateStruct = true, validateCrc = false, throwOnError = false, logOnError = true
    } = options;
    const fileBuffer = await readFile(file);
    const errCb = (throwOnError || logOnError) ? handleErr.bind(null, logOnError, throwOnError) : null;
    const validationWrapper = handleValidation.bind(null, errCb);

    // validate signature
    if (validateStruct && validationWrapper(validatePNGSignature(fileBuffer))) return null;

    const chunks = [];
    const chunksByType = {};
    let chunk, offset = 8, crc = 0, crcBuf;

    const recordNextChunk = () => {
        ({chunk, offset, crc, crcBuf} = extractNextChunk(fileBuffer, offset, validateCrc));
        if (validateCrc && validationWrapper(crcValidation(crcBuf, crc))) return;
        chunks.push(chunk);
        if (chunksByType[chunk.type]) chunksByType[chunk.type].push(chunk);
        else chunksByType[chunk.type] = [chunk];
    }

    // validate first chunk
    if (validateStruct) {
        recordNextChunk();
        if (validationWrapper(validateIHDR(chunks[0]))) return null;
    }

    while (offset < fileBuffer.length) {
        recordNextChunk();
    }

    // validate last chunk
    if (validateStruct && validationWrapper(validateIEND(chunks.at(-1)))) return null;

    return {
        chunks,
        chunksByType
    }
}