Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Join up PNG images to an APNG animated image

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

I've found PHP library only: link

like image 570
don kaka Avatar asked Aug 18 '13 09:08

don kaka


2 Answers

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.

like image 136
Brian Burns Avatar answered Nov 09 '22 06:11

Brian Burns


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)
like image 4
qzb Avatar answered Nov 09 '22 07:11

qzb