Search code examples
node.jsmergediscord.jsgifsharp

Using node js sharp package composite function is not giving an animated result


I have created a simple program using discord.js, what I want is to combine a still image with an image containing animated frames, then the bot will send the result of the animated image and the background of the still image in one image.

const Discord = require('discord.js');
const sharp = require('sharp');
const client = new Discord.Client({ intents: ["GUILDS", "GUILD_MESSAGES"] });

client.on('messageCreate', async message => {
    if (message.content === '!merge') {
        const background = await sharp('logo.png').resize(288, 288);
        const overlay = await sharp('banana.gif').resize(288, 288);

        const result = await background.composite([{ input: await overlay.toBuffer(), gravity: 'center' }]).toBuffer();

        message.channel.send({ files: [result] });
    }
});

client.login('your-token');

But the result was not successful, it sends a static image without the animated frames in GIF format.
I tried to search for a package, but did not find anything.

We hope someone can help us with this, thank you.


Solution

  • In order to create an animated image you must tell sharp to animate the result of the composite.

    According to the documentation the default behavior of composite is to have a default value of animated = false. So what you want to do is tell it which images are animated.

    According to this SO Answer we can composite the image by creating an image roll of the background and compositing that.
    However, when I did this and tried to have the discord bot send the direct buffer data the result was that it was a static image yet... Therefore a work around is to write the image to file send the file then clean up the file after it has been sent.
    Your code adaptation of that answer is here:

    const Discord = require('discord.js');
    const sharp = require('sharp');
    const fs = require('fs'); // we'll use fs to clean up the file afterward.
    const client = new Discord.Client({ intents: ["GUILDS", "GUILD_MESSAGES"] });
    
    async function overlayGif(background, gifOverlay, outFile) { //takes two input paths.
        const backgroundImg = await sharp(background).resize(288, 288);
        const overlay = await sharp(gifOverlay, {animated: true}).resize(288, 288); //make sure animated is true.
        const metadata = await overlay.metadata(); //We'll use the gif metadata to normalize the background.
        const imgRoll = backgroundImg.extend({bottom: 288*(metadata.pages-1), extendWith: 'repeat'}); //Must extend to repeat how ever many pages (frames) are in the gif.
        const result = await imgRoll.composite([
            { input: await overlay.toBuffer(), gravity: 'center', animated: true}
        ]).gif(
            {progressive: metadata.isProgressive, delay: metadata.delay, loop: metadata.loop}
            //Just copying the metadata from the gif to the output format (not sure this is necessary).
        );
        const resInfo = await result.toFile(outFile); //Write the result to a gif file.
        return result;
    }
    
    client.on('messageCreate', async message => {
        if (message.content === '!merge') {
            const tmpFile = (new Date()).getTime()+".gif";
            const result = await overlayGif("logo.png", "banana.gif", tmpFile);
            message.channel.send({ files: [tmpFile]}).then(async (msg)=>{
                fs.unlink(tmpFile, (err)=>{
                    if(err) console.error(err);
                });// clean up the file.
            }).catch(async (err) => {
                fs.unlink(tmpFile, (err)=>{
                    if(err) console.error(err);
                });// clean up file if bot was unable to send.
            });
        }
    });
    
    client.login('your-token');
    

    It appears from your code that you are using a deprecated version of Discord. For those using Discord v14 and onward the code would look like this:

    const Discord = require('discord.js');
    const sharp = require('sharp');
    const Color = require('color');
    const fs = require('fs');
    const client = new Discord.Client({ intents: [
        Discord.GatewayIntentBits.Guilds,
        Discord.GatewayIntentBits.GuildMessages,
        Discord.GatewayIntentBits.MessageContent] 
    });
    async function overlayGif(background, gifOverlay, outFile) { //takes two input paths.
        const backgroundImg = await sharp(background).resize(288, 288);
        const overlay = await sharp(gifOverlay, {animated: true}).resize(288, 288); //make sure animated is true.
        const metadata = await overlay.metadata(); //We'll use the gif metadata to normalize the background.
        const imgRoll = backgroundImg.extend({bottom: 288*(metadata.pages-1), extendWith: 'repeat'}); //Must extend to repeat how ever many pages (frames) are in the gif.
        const result = await imgRoll.composite([
            { input: await overlay.toBuffer(), gravity: 'center', animated: true}
        ]).gif(
            {progressive: metadata.isProgressive, delay: metadata.delay, loop: metadata.loop}
            //Just copying the metadata from the gif to the output format (not sure this is necessary).
        );
        const resInfo = await result.toFile(outFile); //Write the result to a gif file.
        return result;
    }
    
    client.on('messageCreate', async message => {
        console.log(message.content);
        if (message.content === '!merge') {
            const tmpFile = (new Date()).getTime()+".gif";
            const result = await overlayGif("logo.png", "banana.gif", tmpFile);
            message.channel.send({ files: [tmpFile]}).then(async (msg)=>{
                fs.unlink(tmpFile, (err)=>{
                    if(err) console.error(err);
                });// clean up the file.
            }).catch(async (err) => {
                fs.unlink(tmpFile, (err)=>{
                    if(err) console.error(err);
                });
            });
        }
    });
    
    client.login("super-secret-token");
    

    In this example I'm using the current time to create a "unique" file name. Please use an appropriate package for making the files truly unique and make sure to employ proper error handling. This code is for demonstrative purposes (as a starting point).