node.jsimagemagickgraphicsmagickgmnode-imagemagick

Creating a GIF from remote stream in graphicsmagick


I am creating a GIF from remote files in node currently by downloading each image to the file system into a tmp folder.

I want to bypass saving the image to a tmp folder and save in memory instead. Is this possible?

As you can see, I have a download function in my AWS class which saves to a tmp folder:

download(key){
    return new Promise((resolve, reject) => {
      request.head(`${this.base_url}/${this.bucket}/${key}`, (err, res, body) => {
        request(`${this.base_url}/${this.bucket}/${key}`)
            .pipe(fs.createWriteStream(`tmp/${key}`)).on('close', resolve )
      })
    })
};

Once they have all downloaded, I have a createGif function in my GifService class which adds each file path as a custom argument of gm, adds a delay of 50ms, resizes then outputs as buffer which I am then uploading to AWS s3.

import gm from 'gm';

...

constructor(){
    this.gm = gm()
}

generateGif(images, prefix){

    return new Promise((resolve, reject) => {

        // for each image we want in array, we pass to gm
        images.forEach(image => {
            this.gm.in(`tmp/${image.Key}`)
        })

        // Now we create the gif with 50sec delay between images, sized to 600px x 2
        this.gm
            .delay(50)
            .resize(600,600)
            .toBuffer('gif', async (err, buffer) => {
        if (err) reject(err)

        const params = {
                ACL: 'public-read',
                Bucket: config.aws_bucket,
                ContentType: 'image/gif',
                Key: `${prefix}/${uuid()}.gif`,
                Body: buffer
              }

              try{
                // uplaod to S3
            const upload = await this.aws.upload(params)

            // resolve s3 URL
            resolve(upload)

              }catch(err) {
                console.log('err', err)
                reject(err)
              }

      });
    })

}

Ideally if I could pass a remote file stream as custom argument, or pass a buffer in as a custom argument as opposed to how I am currently passing in the tmp file path:

images.forEach(image => {
    this.gm.in(`tmp/${image.Key}`)
})

Solution

  • I managed to make it work using only streams by converting first the images to miff and concat them into a single stream. Then passing the buffer or the stream into gm again with delay does the trick.

    You will need to instal concat-stream npm for this to work.

    Sorry for the mixed ES5 code.

    import gm from 'gm';
    var concat = require('concat-stream');
    ...
    
    constructor() {
        this.gm = gm()
    }
    
    start() {
    
        return getYourReadAbleStreamsSomehow().then(streams => {
    
            return generateGif(streams);
    
        }).then(gifBuffer => {
    
            return uploadToAWS(gifBuffer, prefix);
    
        }).catch(err => {
            console.log(err)
        })
    
    }
    
    uploadToAWS(buffer, prefix) {
        const params = {
            ACL: 'public-read',
            Bucket: config.aws_bucket,
            ContentType: 'image/gif',
            Key: `${prefix}/${uuid()}.gif`,
            Body: buffer
        }
    
        try {
            // uplaod to S3
            const upload = await this.aws.upload(params)
    
            // resolve s3 URL
            resolve(upload)
    
        } catch (err) {
            console.log('err', err)
            reject(err)
        }
    }
    
    generateGif(imageStreams, delay) {
    
        return new Promise((resolve, reject) => {
    
            var write = concat(function(buffer) {
                gm(buffer)
                    .delay(delay)
                    .toBuffer('gif', function(err, buffer) {
    
                        if (err) 
                            reject(err);
    
                        resolve(buffer);
    
                    })
            })
    
            //Convert to miff and concat streams
            var i = 0;
            var streamHandler = function() {
    
                gm(imageStreams[i])
                    .resize('600', '600')
                    .stream('miff', function(err, stdout, stderr) {
                        if (err)
                            reject(err)
    
                        var lastOne = i === streams.length - 1;
    
                        if (!lastOne)
                            stdout.once('end', streamHandler)
    
                        stdout.pipe(write, {
                            end: lastOne
                        });
    
                        i++;
    
                    });
            }
    
            streamHandler();
    
        })
    
    }