src/helpers/makeApng.js

/**
 * @file
 * @brief Vendored APNG encoder (TomasHubelbauer / node-apng, MIT): build an animated PNG from frame buffers via acTL / fcTL / fdAT chunks and crc.
 */

import crc from "crc";

// CREDIT: Github TomasHubelbauer / node-apng
// LICENSE: MIT

// I can't use npm install directly since it installs unesesary code that
// has securtiy vulnerabiltiies
// Also it's not fit for module.exports

// This is copied from the repo with permission from the MIT license

export default function makeApng(buffers, delay) {
  function findChunk(buffer, type, offset = 8) {
    while (offset < buffer.length) {
      const chunkLength = buffer.readUInt32BE(offset);
      const chunkType = buffer.slice(offset + 4, offset + 8).toString("ascii");

      if (chunkType === type) {
        return buffer.slice(offset, offset + chunkLength + 12);
      }

      offset += 4 + 4 + chunkLength + 4;
    }

    return null;
  }

  const actl = Buffer.alloc(20);
  actl.writeUInt32BE(8, 0); // Length of chunk
  actl.write("acTL", 4); // Type of chunk
  actl.writeUInt32BE(buffers.length, 8); // Number of frames
  actl.writeUInt32BE(0, 12); // Number of times to loop (0 - infinite)
  actl.writeUInt32BE(crc.crc32(actl.slice(4, 16)), 16); // CRC

  let sequenceNumber = 0;
  const frames = buffers.map((data, index) => {
    const ihdr = findChunk(data, "IHDR");
    if (ihdr === null) {
      throw new Error("IHDR chunk not found!");
    }

    const fctl = Buffer.alloc(38);
    fctl.writeUInt32BE(26, 0); // Length of chunk
    fctl.write("fcTL", 4); // Type of chunk
    fctl.writeUInt32BE(sequenceNumber++, 8); // Sequence number
    fctl.writeUInt32BE(ihdr.readUInt32BE(8), 12); // Width
    fctl.writeUInt32BE(ihdr.readUInt32BE(12), 16); // Height
    fctl.writeUInt32BE(0, 20); // X offset
    fctl.writeUInt32BE(0, 24); // Y offset
    const { numerator, denominator } = delay(index);
    fctl.writeUInt16BE(numerator, 28); // Frame delay - fraction numerator
    fctl.writeUInt16BE(denominator, 30); // Frame delay - fraction denominator
    fctl.writeUInt8(0, 32); // Dispose mode
    fctl.writeUInt8(0, 33); // Blend mode
    fctl.writeUInt32BE(crc.crc32(fctl.slice(4, 34)), 34); // CRC

    let offset = 8;
    const fdats = [];
    while (true) {
      const idat = findChunk(data, "IDAT", offset);
      if (idat === null) {
        if (offset === 8) {
          throw new Error("No IDAT chunks found!");
        } else {
          break;
        }
      }

      offset = idat.byteOffset + idat.length;

      // All IDAT chunks except first one are converted to fdAT chunks
      if (index === 0) {
        fdats.push(idat);
      } else {
        const length = idat.length + 4;
        const fdat = Buffer.alloc(length);
        fdat.writeUInt32BE(length - 12, 0); // Length of chunk
        fdat.write("fdAT", 4); // Type of chunk
        fdat.writeUInt32BE(sequenceNumber++, 8); // Sequence number
        idat.copy(fdat, 12, 8); // Image data
        fdat.writeUInt32BE(crc.crc32(fdat.slice(4, length - 4)), length - 4); // CRC
        fdats.push(fdat);
      }
    }

    return Buffer.concat([fctl, ...fdats]);
  });

  const signature = Buffer.from("89504e470d0a1a0a", "hex");
  const ihdr = findChunk(buffers[0], "IHDR");
  if (ihdr === null) {
    throw new Error("IHDR chunk not found!");
  }

  const iend = Buffer.from("0000000049454e44ae426082", "hex");
  return Buffer.concat([signature, ihdr, actl, ...frames, iend]);
}