Created
August 30, 2016 20:55
-
-
Save qzb/3c83ad206bbbe170d65506c2f081c613 to your computer and use it in GitHub Desktop.
Merges multiple PNG images into single APNG animation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm trying to get this working in a web app with webpack.
I had to change this line since it caused errors:
I'm guessing it's supposed to say
But (after setting up the wrappers needed to convert images to buffers and such,) the output I'm getting isn't animated.
Opening it in TweakPNG I get this error and it shows the chunks up to where the first fdAT chunk is supposed to be, which seems to indicate the fdAT chunk length is incorrect and would put it beyond the end of the file, or perhaps just to a position that isn't the start of a chunk so it's reading a random value as a chunk length.
Do you have a working version of this code, perchance with minor differences that didn't end up here (or on SO)?