Search code examples
node.jsbufferqgraphicsmagick

Very random behaviour in Node with gm, buffers and promises


I recently switched my image saving module over to gm (grahicsmagick) and began using buffers instead of saving to disk.

The output I expect is an array with an md5 hash, then the paths to the original image and the thumbnail. I use promises, like this.

saveOrig( imageUrl )
.then( saveThumb )
.then( function( image ) {
    var returnArray = [ image.hash, image.orig, image.thumb ]
    console.log( returnArray )
    resolve( returnArray )
})
.catch( function( error ) {
    reject( new Error( error.message ) )
})

Here is the first function, the next one is almost identical

function saveOrig ( imageUrl ) {
    return Q.Promise( function ( resolve, reject, notify ) {

        var image = {
            extension: path.extname( imageUrl )
        }

        gm( request( imageUrl ) )
            .format( function( err, value ) {
                if ( err ) return reject ( new Error ( err ) )

                image.type = value
            })
            .stream( image.type, function ( err, stdout, stderr ) {
                if ( err ) return reject( new Error( err ) )

                var bufs = []

                stdout.on( 'data', function ( d ) {
                    bufs.push( d )
                })

                stdout.on( 'end', function () {
                    var buf = Buffer.concat( bufs )

                    image.hash = crypto.createHash( 'md5' ).update( buf ).digest( 'hex' )

                    console.log ( image.hash )

                    uploader = s3Client.putBuffer( buf, type + "/" + image.hash + "-orig" + image.extension, {
                        'Content-Length': buf.length,
                        'Content-Type': 'image/jpeg'
                    }, function ( err, result ) {
                        if ( err ) return reject( new Error( err ) )

                        if ( result.statusCode == 200 ) {
                            image.orig = uploader.url

                            resolve( image )
                        }
                    })
                })
            })
    })
}

Again, here's what I expect to see,

[ '820f841a0a7cdc854b70f8b534dc7705',
'https://my-amazon-bucket.s3.amazonaws.com/read/820f841a0a7cdc854b70f8b534dc7705-orig.jpeg',
'https://my-amazon-bucket.s3.amazonaws.com/read/820f841a0a7cdc854b70f8b534dc7705-thumb.jpeg' ]

That's what happens when I process just one image. But when I call this function mapped to an array with Q.all, I get extremely random seeming mixtures of hashes, thumbnails and original paths, presumably from other calls to the function before it.

I didn't have this behaviour before when I wasn't using buffers or gm. What's the cause of this?


Edit: here's how I call the saveImage function described above. This seemed to work fine when I was saving items to disk, then manipulating them with the easy-image module.

images = window.document.getElementsByTagName( 'img' )

imageMapFunction = Array.prototype.map.call( images, function ( each, index ) {
    return Q.promise( function ( resolve, reject, notify ) {


    saveImage( req.body.type, each.src )
    .spread( function ( imageHash, imageOriginalPath, imageThumbPath ) {
        article.images.push({
            image: imageOriginalPath,
            imageHash: imageHash,
            imageThumb: imageThumbPath
        })

        each.src = imageOriginalPath

        resolve()
    })

    })
})

Q.all( imageMapFunction )
.then( function () {

Solution

  • This section is incorrect:

    uploader = s3Client.putBuffer( buf, type + "/" + image.hash + "-orig" + image.extension, {
        'Content-Length': buf.length,
        'Content-Type': 'image/jpeg'
    }, function ( err, result ) {
        if ( err ) return reject( new Error( err ) )
    
        if ( result.statusCode == 200 ) {
            image.orig = uploader.url
    
            resolve( image )
        }
    });
    

    You need a var in there as var uploader.

    As it is now, if you call your function multiple times, you will overwrite a global uploader every time, so the final results you get back will depend on how long each image takes to process and how long they take to upload.