Join up PNG images to an APNG animated image

4.5k views Asked by At

Is it possible somehow to join up PNG images to an APNG animated image using nodejs?

I've found PHP library only: link

4

There are 4 answers

1
Brian Burns On

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 file

UPNG.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.

2
qzb On

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:

  1. Take all chunks of the first PNG file as a building basis.
  2. Insert an animation control chunk (acTL) after the image header chunk (IHDR).
  3. If the first PNG is to be part of the animation, insert a frame control chunk (fcTL) before the image data chunk (IDAT).
  4. 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)
1
AudioBubble On

I'm not sure about nodejs, but you could try APNG-canvas. APNG uses HTML5 (-webkit-canvas), JavaScript (jQuery).

"APNG-canvas is a library for displaing Animated PNG files in the browsers with canvas support (Google Chrome, Internet Explorer 9, Apple Safari)."

Working demo is here.

0
Ben On

Currently, no it doesn't look like it. Wikipedia lists the available software, and as you can see there's no support for ImageMagick which has a Node wrapper. However, you may find you can download the command line tool apngasm and shell out to it, if you find it's worth your while there are Node command line wrappers to hook this into an existing application using child_process (http://nodejs.org/api/child_process.html).