Search code examples
javascriptnode.jspromisegm

Use node gm with promises and buffers


I've been trying to use gm with Bluebird like this:

var gm = require('gm');
var bluebird = require('bluebird');
gm = bluebird.promisifyAll(gm);

But then, when I try something like this:

gm(req.file.buffer)
     .crop(tWidth, tHeight, tWidth/2, tHeight/2)
     .gravity('Center')
     .toBuffer()
  .then(...)

I get this error:

gm().toBuffer() expects a callback.

If I only promisify the buffer method:

bluebird.promisify(gm(req.file.buffer)
        .crop(tWidth, tHeight, tWidth/2, tHeight/2)
        .gravity('Center')
        .toBuffer)()
        .then(buff => {...})

I get this error:

TypeError: this.stream is not a function 6:36:21 AM web.1 | at toBuffer (/Users/danielrvt/IdeaProjects/twe-backend/node_modules/gm/lib/command.js:162:17)

If I don't use promises, it works just fine.


Solution

  • That's not really how you're supposed to use the gm module as it is designed to provide a factory gm() and a number of cascadeable mutator functions like .resize().

    Bluebird's .promisifyAll() only works on functions that accept a callback, which the majority of gm's functions do not.

    If you wish to use gm with promises, you'll need to "promisify" it yourself, by wrapping your call as

    function mutateAndSave() {
      return new Promise( function(resolve,reject) {
        try {
          gm(image)
            .doSomething()
            .write(outputPath, function(err) {
              if(err) {
                throw(err);
              }
              resolve();
            });
        }
        catch (err) {
          reject(err);
        }
      });
    }
    

    After which, you can

    mutateAndSave()
      .then(...)
      .catch(...);
    

    UPDATE

    Here are two ways to do what you want, but...

    You'll note that both are a lot more complicated than just using gm as it is intended. ;)

    Here's a way to do what you want with an event state machine.

    const gm = requre('gm');
    const EventEmitter = require('events');
    const input_path = './image.jpg'
    const output_path = './newimage.jpg';
    const worker = new EventEmitter(); // create an event worker
    
    // define the worker states - note: NO error checking! Muy mal! 
    const event_states={
      start:()       => worker.emit('resize',gm(input_path)),       // creates a new gm image instance
      resize:(img)   => worker.emit('crop', img.resize(100,100)),   // resizes the image
      crop:(img)     => worker.emit('write', img.crop(2,2,50,50)),  // crops it
      write:(img)    => {                                           // and writes it to the filesystem
        img.write(output_path, err => {
          if(err) {
            worker.emit('error',err);
            return;
          }
          worker.emit('complete');
        });
      },
      error: (err)  => console.error(err.toString()),             // report error
      complete: ()  => console.log('complete')                    // alert on completion
    };
    
    // add the states to the worker as event handlers
    Object.keys(event_states).forEach(k => worker.on(k, event_states[k]));
    
    // and fire it up...
    worker.emit('start'); 
    

    Or if you really, REALLY want to use Promises...

    const writer = function (img) {
      return new Promise( (resolve, reject) => {
        img.write(output_path,err => {
          if(err) {
            reject(err);
            return;
          }
          resolve(true);
        });
      });
    };
    
    const reader = function (input_path) {
      return new Promise( (resolve,reject) => { 
        let img;
        try {
          img = gm(input_path);
        }
        catch (err) {
          reject(err);
          return;
        }
        resolve(img);
      });
    };
    
    reader('./image.jpg')
      .then( img => { return img.resize(100,100) }) // the return here is for illustration only
      .then( img => img.crop(2,2,50,50))            // since this does exactly the same thing with less typing!
      .then( writer )
      .then( res => console.log('complete'))
      .catch( err => console.error(err.toString()));
    

    Again, lots more typing and complexity all to use the newest "shiny" thing. ;)