Is it possible somehow to join up PNG images to an APNG animated image using nodejs?
I've found PHP library only: link
UPNG.js can parse and build APNG files - https://github.com/photopea/UPNG.js
From the readme -
UPNG.js supports APNG and the interface expects "frames".
UPNG.encode(imgs, w, h, cnum, [dels])
imgs: array of frames. A frame is an ArrayBuffer containing the pixel data (RGBA, 8 bits per channel) w, h : width and height of the image cnum: number of colors in the result; 0: all colors (lossless PNG) dels: array of delays for each frame (only when 2 or more frames) returns an ArrayBuffer with binary data of a PNG fileUPNG.js can do a lossy minification of PNG files, similar to TinyPNG and other tools. It performs color quantization using the k-means algorithm.
Lossy compression is allowed by the last parameter cnum. Set it to zero for a lossless compression, or write the number of allowed colors in the image. Smaller values produce smaller files. Or just use 0 for lossless / 256 for lossy.
There is no library for that, but it is quite simple to implement. Algorithm for merging multiple PNG files into single APNG is described in Wikipedia:
- Take all chunks of the first PNG file as a building basis.
- Insert an animation control chunk (acTL) after the image header chunk (IHDR).
- If the first PNG is to be part of the animation, insert a frame control chunk (fcTL) before the image data chunk (IDAT).
- For each of the remaining frames, add a frame control chunk (fcTL) and a frame data chunk (fdAT). Then add the image end chunk (IEND). The content for the frame data chunks (fdAT) is taken from the image data chunks (IDAT) of their respective source images.
Here is an example implementation:
const fs = require('fs')
const crc32 = require('crc').crc32
function findChunk(buffer, type) {
  let offset = 8
  while (offset < buffer.length) {
    let chunkLength = buffer.readUInt32BE(offset)
    let chunkType = buffer.slice(offset + 4, offset + 8).toString('ascii')
    if (chunkType === type) {
      return buffer.slice(offset, offset + chunkLength + 12)
    }
    offset += 4 + 4 + chunkLength + 4
  }
  throw new Error(`Chunk "${type}" not found`)
}
const images = process.argv.slice(2).map(path => fs.readFileSync(path))
const actl = Buffer.alloc(20)
actl.writeUInt32BE(8, 0)                                    // length of chunk
actl.write('acTL', 4)                                       // type of chunk
actl.writeUInt32BE(images.length, 8)                        // number of frames
actl.writeUInt32BE(0, 12)                                   // number of times to loop (0 - infinite)
actl.writeUInt32BE(crc32(actl.slice(4, 16)), 16)            // crc
const frames = images.map((data, idx) => {
  const ihdr = findChunk(data, 'IHDR')
  const fctl = Buffer.alloc(38)
  fctl.writeUInt32BE(26, 0)                                 // length of chunk
  fctl.write('fcTL', 4)                                     // type of chunk
  fctl.writeUInt32BE(idx ? idx * 2 - 1 : 0, 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
  fctl.writeUInt16BE(1, 28)                                 // frame delay - fraction numerator
  fctl.writeUInt16BE(1, 30)                                 // frame delay - fraction denominator
  fctl.writeUInt8(0, 32)                                    // dispose mode
  fctl.writeUInt8(0, 33)                                    // blend mode
  fctl.writeUInt32BE(crc32(fctl.slice(4, 34)), 34)          // crc
  const idat = findChunk(data, 'IDAT')
  // All IDAT chunks except first one are converted to fdAT chunks
  let fdat;
  if (idx === 0) {
    fdat = idat
  } else {
    const length = idat.length + 4
    fdat = Buffer.alloc(length)
    fdat.writeUInt32BE(length - 12, 0)                      // length of chunk
    fdat.write('fdAT', 4)                                   // type of chunk
    fdat.writeUInt32BE(idx * 2, 8)                          // sequence number
    idat.copy(fdat, 12, 8)                                  // image data
    fdat.writeUInt32BE(crc32(4, length - 4), length - 4)    // crc
  }
  return Buffer.concat([ fctl, fdat ])
})
const signature = Buffer.from('\211PNG\r\n\032\n', 'ascii')
const ihdr = findChunk(images[0], 'IHDR')
const iend = Buffer.from('0000000049454e44ae426082', 'hex')
const output = Buffer.concat([ signature, ihdr, actl, ...frames, iend ])
fs.writeFileSync('output.png', output)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With