gm icon indicating copy to clipboard operation
gm copied to clipboard

Getting "toBuffer() Stream yields empty buffer" error for append function

Open Siddeshgad opened this issue 7 years ago • 38 comments

Here is my sample code var gm = require('gm').subClass({ imageMagick: true }); // Enable ImageMagick integration.

gm(imgSrc1).append(imgSrc2, true).setFormat('jpeg').toBuffer(function(err, buffer) { if (err) { next(err); } else { next(null, buffer); } });

Env: AWS Lambda Memory : 1024MB (To make sure I'm not running out of memory while testing)

The same code works on my EBS instance.

Let me know if image appending can be achieved in AWS lambda.

Siddeshgad avatar Aug 24 '16 19:08 Siddeshgad

@Siddeshgad Have you been able to get this to work? I'm having the same issue.

amitaymolko avatar Jan 05 '17 13:01 amitaymolko

@amitaymolko Not yet. I had a work around for this problem. I am processing images with this function on my server rather than automating it using AWS Lambda service.

This problem was only occurring on AWS Lambda instances. I was able to execute the same code on my local machine as well as on server.

Siddeshgad avatar Jan 10 '17 13:01 Siddeshgad

I was able to get it to work like this:

var image = gm(body, pathname)
image
	.setFormat(mime.extension(contentType))
	.resize(width, height, '^')
	.gravity('Center')
	.crop(width, height)
	.stream(function(err, stdout, stderr) {
		if(err) reject(err)
		var chunks = [];
	        stdout.on('data', function (chunk) {
	            chunks.push(chunk);
	        });
		stdout.on('end', function () {
			var image = Buffer.concat(chunks);
			var s3_options = {
				Bucket: S3_BUCKET,
				Key: bucketkey,
				Body: image,
				ACL: 'public-read',
				ContentType: contentType,
				ContentLength: image.length
			}
			S3.upload(s3_options, (err, data) => {
				if(err) {
					reject(err)
				}
				var image_url = `${STORAGE_BASE_URL}/${bucketkey}`
				resolve({original_url: url, image_url: image_url, size})

			})
        	});
		stderr.on('data',function(data){
			console.log(`stderr ${size} data:`, data);
		})
	})

As you can see this also includes the S3 upload code. Hope this can help you.

amitaymolko avatar Jan 10 '17 16:01 amitaymolko

I am getting this error locally now, on OSX. It suddenly started happening and I have no idea why. Digging in...

jescalan avatar Apr 13 '17 03:04 jescalan

Ok, so my issue is that there was an error in the processing (for me, image path to a composited image was incorrect), however the toBuffer method will always return with this "empty buffer" message rather than actually showing you the real error you need to fix.

To improve on this, I used some of @amitaymolko's great code above to create a better, promise-based implementation of the toBuffer method that actually returns errors correctly if there are any:

function gmToBuffer (data) {
  return new Promise((resolve, reject) => {
    data.stream((err, stdout, stderr) => {
      if (err) { return reject(err) }
      const chunks = []
      stdout.on('data', (chunk) => { chunks.push(chunk) })
      // these are 'once' because they can and do fire multiple times for multiple errors,
      // but this is a promise so you'll have to deal with them one at a time
      stdout.once('end', () => { resolve(Buffer.concat(chunks)) })
      stderr.once('data', (data) => { reject(String(data)) })
    })
  })
}

If any maintainers of this project are checking this out, I would be happy to PR this into the core but as a callback-based version, just let me know if you want me to!

jescalan avatar Apr 13 '17 03:04 jescalan

I got the same error on heroku too.

However, toBuffer works fine under cedar-14 (ubuntu 14.04) + https://github.com/ello/heroku-buildpack-imagemagick (6.9.5-10) but return error after upgrade to heroku-16 (ubuntu 16.04)

Still don't know the root cause of this issue, so we cannot use toBuffer under new version?

siygle avatar Apr 28 '17 16:04 siygle

@Ferrari -- I would use the version in my comment above to be sure that you are getting accurate error messages at least until it the library is patched (any maintainers listening in? I'm happy to submit a patch as long as someone gives me the go ahead here)

jescalan avatar Apr 28 '17 16:04 jescalan

@jescalan What's the best way to use your gmToBuffer function? I'm not sure how to use it... what should I pass as the data attribute?

thomasjonas avatar May 01 '17 08:05 thomasjonas

Hey @thomasjonas -- you can pass the results of a gm call directly into it 😁. So for example:

const data = gm(srcStream, 'output.jpg').resize(500)
gmToBuffer(data).then(console.log)

jescalan avatar May 01 '17 13:05 jescalan

Thanks so much @jescalan . You saved the day :smile_cat:

Just in case anybody needs to fetch from a url, process and convert to base64, this is how I finally did it.

const data = gm(request(url)).borderColor('#000').border(1,1).resize(500,500)
gmToBuffer(data).then(function(buffer) {
    let dataPrefix = `data:image/png;base64,`
    let data = dataPrefix + buffer.toString('base64')
    return callback(null, data)
})
.catch(function(err){
    console.log(err)
    return callback(err)
})

Sidenote: AWS Lambda was crashing because I had const gm = require('gm') instead of const gm = require('gm').subClass({imageMagick: true})

Diferno avatar Jun 28 '17 10:06 Diferno

@jescalan 's code works for me too on Google Cloud with CentOS. The only reasonable explanation I can think of is that the toBuffer function resolves to some underlying C code that has compatibility issues - building the buffer from the stream goes around that problem.

moshewe avatar Jan 21 '18 15:01 moshewe

Hi @jescalan I am having a problem with windows. I have finished my app and it works perfectly if I run it from cmd.exe with "node myapp.js".

The problem though is that I need to put it in a scheduled task so it can be run every x hours. When the scheduler run my task I keep getting the same error. Stream yields empty buffer.

So I use your function I got a different one. Invalid Parameter -

Any ideas?.

var fs = require('fs')
  , gm = require('gm').subClass({imageMagick: true});
 

let fp = `${__dirname}/logo.png`
let new_fp = `${__dirname}/logo_new.png`

//resize and remove EXIF profile data
let data = gm(fp)
.resize(240, 240)
.noProfile()
.write(new_fp, function (err) {
  if (!err){
    console.log('done')
  }else{
    console.log(err);
    
    }
})

function gmToBuffer (data) {
    return new Promise((resolve, reject) => {
      data.stream((err, stdout, stderr) => {
        if (err) { return reject(err) }
        const chunks = []
        stdout.on('data', (chunk) => { chunks.push(chunk) })
        // these are 'once' because they can and do fire multiple times for multiple errors,
        // but this is a promise so you'll have to deal with them one at a time
        stdout.once('end', () => { resolve(Buffer.concat(chunks)) })
        stderr.once('data', (data) => { reject(String(data)) })
      })
    })
  }

gmToBuffer(data).then(console.log).catch(e => console.log(e))

Jucesr avatar Sep 14 '18 17:09 Jucesr

Might be because you're calling write on your original gm process call? It's also possible you have an error in your path and there is genuinely no data.

jescalan avatar Sep 14 '18 17:09 jescalan

@jescalan I got a new error. Invalid Parameter - -resize. Looks windows task scheduler can't reference this method?.

I am lost here

Jucesr avatar Sep 14 '18 17:09 Jucesr

I don't really think it has anything to do with windows or windows task scheduler based on that error message. I'm not entirely sure what's going wrong at this point though, nor did I write or maintain this library so my knowledge of how it works is limited. I'm sorry!

jescalan avatar Sep 14 '18 18:09 jescalan

@jescalan I uninstalled imagemagick and I am getting the same error when I run it with node.

So it was not finding imagemagick when I was running with the scheduler. I don't know what to do though.

Jucesr avatar Sep 14 '18 18:09 Jucesr

Imagemagick installation problems related to windows scheduler is way out of my range of abilities to debug. I don't use windows at all, nor do I have access to your machine or the ability to tinker with the command line. The only possible suggestion I have here is that this is not an imagemagick library, its graphicsmagick -- if you installed imagemagick instead then it would be pretty clear why it wouldn't be working.

jescalan avatar Sep 14 '18 18:09 jescalan

@jescalan Finally I was able to fix it. Thanks!. (https://github.com/aheckmann/gm/issues/572#issuecomment-421423019) pointed me to the right direction.

Instead of setting the task to run node myapp.js. I told the task to run cmd.exe /k cd "C:\my\path" & node myapp.js

Jucesr avatar Sep 14 '18 19:09 Jucesr

Amazing! So glad this worked out, congrats 🎉

jescalan avatar Sep 14 '18 21:09 jescalan

I got this issue on Heroku when using heroku-18 stack together with ello/heroku-buildpack-imagemagick.

Removing the buildpack solved the issue for me. Seems like heroku-18 comes with imagemagick installed already. You can check with this command:

$ heroku run convert --version

ruanmartinelli avatar Jan 14 '19 18:01 ruanmartinelli

Also getting this on Heroku 18 now, with or without additional buildpacks.

adamreisnz avatar Jan 28 '19 10:01 adamreisnz

This buildpack worked for me: https://github.com/bogini/heroku-buildpack-graphicsmagick

adamreisnz avatar Jan 28 '19 11:01 adamreisnz

I'm still getting the same error when trying to use streams as specified in the docs or even when using the code suggested above.

I already tried the following with no luck:

  • with http options tweaks
  • with Imagemagick
  • with Graphicsmagick
  • with an additional PassThrough stream used as a writeStream as the docs suggests.

This is the error I'm getting:

[2019-01-30T14:35:48+10:00] (2019/01/30/[$LATEST]2c3e75a349ac41c1ad821b37b9f3a9ee) 2019-01-30T04:35:48.960Z 389a0660-49f6-4cd8-a9f4-567d783bba56 {"errorMessage":"gm convert: Read error at scanline 7424; got 3792644 bytes, expected 9216000. (TIFFFillStrip).\n"}

Doesn't seem to be an issue with AWS SDK since if I just create a download steam with S3.getObject.createReadStream and pass it to S3.upload method it works perfectly. Meaning even with a huge file of 3gb, simply copying from/to s3 works without any issues.

The following code works with small images but it fails with a image of 1.5gb when deployed and running from AWS.

import {Handler} from "aws-lambda";
import {Helper} from "../lib/Helper";
import * as AWS from "aws-sdk";

let https = require("https");
let agent = new https.Agent({
    rejectUnauthorized: true,
    maxSockets: 1000
});

AWS.config.update({
    "httpOptions": {
        timeout: 600000,
        connectTimeout: 600000,
        logger: console,
        agent: agent
    }
});

export const handler: Handler = async (event) => {
    process.env["PATH"] = process.env["PATH"] + ":" + process.env["LAMBDA_TASK_ROOT"];

    const {bucket, object} = event.Records[0].s3;

    let total = 0;
    const file = Helper
        .getS3Client()
        .getObject({Bucket: bucket.name, Key: object.key})
        .createReadStream()
        .on("data", (data) => {
            total += data.toString().length;
            // console.log(`s3 stream read ${data.byteLength} bytes @ ${Date.now()}`);
        })
        .on("error", (err) => {
            console.log(`s3 stream <${object.key}>: ${err}`);
        })
        .on("finish", () => {
            console.log(`s3 stream finished ${object.key}`);
        })
        .on("end", () => {
            console.log(`s3 stream successfully downloaded ${object.key}, total ${Helper.bytesToSize(total)}`);
        });

    const gm = require("gm").subClass({imageMagick: false});

    const img = await new Promise((resolve, reject) => {
        gm(file, Helper.getFilenameFromS3ObjectKey(object.key))
            .resize(
                Helper.THUMBNAIL_WIDTH,
                Helper.THUMBNAIL_HEIGHT
            )
            .quality(Helper.THUMBNAIL_QUALITY)
            .stream("jpg", (err, stdout, stderr) => {
                if (err) {
                    return reject(err);
                }
                const chunks = [];
                stdout.on("data", (chunk) => {
                    chunks.push(chunk);
                });
                stdout.once("end", () => {
                    resolve(Buffer.concat(chunks));
                });
                stderr.once("data", (data) => {
                    reject(String(data));
                });
            });
    });

    const promise = new Promise((resolve, reject) => {
        Helper
            .getS3Client()
            .upload({
                Body: img,
                Bucket: bucket.name,
                Key: Helper.getThumbFilename(object.key)
            })
            .promise()
            .then(() => {
                agent.destroy();
                resolve();
            })
            .catch((err) => {
                console.log(err);
            });
    });

    await promise;
};

Interestingly enough It does work fine, even with a 1.5gb image, when testing locally with lamci/lambda:

docker run -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY -v "$PWD/dist":/var/task lambci/lambda:nodejs8.10 index.handler '{ "Records": [{ "s3": { "bucket": { "name": "xyz-images" }, "object": { "key": "bigfile.tiff" } } }] }'

Looks like my box is fast enough to consume the readStream created by S3.getObject.createReadStream and convert the image, whereas the box in AWS isn't and messes up the stream flow.

marcelopm avatar Jan 30 '19 04:01 marcelopm

I'm finding that I get this error due to the initial image resolution rather than just the file size. My script can handle a 30mb file fine at 72dpi but I get the empty buffer message on a 12mb image at 400dpi. The same image uploaded at 350dpi processes just fine. I've increased the memory allocation for the Lambda function but still haven't had success with 400dpi images. I'm unfamiliar with what is behind the curtain of ImageMagick but could this be a limitation in the size of the buffer created by high-resolution images?

truehello avatar Mar 15 '19 22:03 truehello

A little more digging reveals that there might be a cap to the buffer size in imageMagick. https://forums.aws.amazon.com/thread.jspa?threadID=174231 Not sure if increasing the Lambda timeout and memory will alleviate this, or it is a limitation of imageMagic.

truehello avatar Mar 15 '19 22:03 truehello

We moved all our image processing to kraken.io to avoid running into the constant issues related to running imagemagick on our own server. Would highly recommend it if all you're doing is resizing/scaling/optimising images.

adamreisnz avatar Mar 16 '19 05:03 adamreisnz

I'm see the ImageMagick library have a parameter -limit memory xxx and it's work fine with command line tool, but it's not working in gm library.

when I see the source about gm, I'm found the limit function in args.js

proto.limit = function limit (type, val) {
    type = type.toLowerCase();

    if (!~limits.indexOf(type)) {
      return this;
    }

    return this.out("-limit", type, val);
  }

Do you see, it's called this.out function, that means it's will be append the -limit parameter after to source, like thisconvert xxx.jpg -limit memory 512 and not to convert -limit memory 512 xxx.jpg, so we just need change the code to :

return this.in("-limit", type, value);

It's working now... my test picture resolution is: 20386x8401

jhondge avatar Apr 30 '19 03:04 jhondge

This might be unrelated but I didn't start having this buffer error until I changed my node version in lambda from like 6 to 10. I think this might actually be an issue with bumping the node version.

EDIT: CONFIRMED. My problems went away entirely by downgrading to nodejs8.10

VictorioBerra avatar Aug 01 '19 23:08 VictorioBerra

I'm experiencing the same issue. First it was because I was using toBuffer method. Then I changed to .stream method but it was returning an empty stream. I added console.logs at on('data', ... and on('end', ... to verify and it was only printing end.

Then I changed imageMagick from true to false:

Before:

const gm = require('gm').subClass({ imageMagick: true });

After:

const gm = require('gm').subClass({ imageMagick: false });

and it started working, but only in my local machine.

I executed the function using SAM CLI but it started uploading files with 0 size.

I uploaded code to AWS Lambda and the same is happening, it uploads files with 0 size. Only in my local machine is working.

This is my complete code:

const gm = require('gm').subClass({ imageMagick: false });
const AWS = require('aws-sdk');

const s3 = new AWS.S3();

exports.handler = async (event, context, cb) => {
  const validExtensions = ['jpg', 'jpeg', 'png'];

  const { bucket, object } = event.Records[0].s3;

  // Where images are uploaded
  const origin = 'original/';

  // Where optimized images will be saved
  const dest = 'thumbs/';

  // Object key may have spaces or unicode non-ASCII characters. Remove prefix
  const fullFileName = decodeURIComponent(object.key.replace(/\+/g, ' '))
    .split('/').pop();

  const [fileName, fileExt] = fullFileName.split('.');

  if (!validExtensions.includes(fileExt)) {
    return cb(null, `Image not processed due to .${fileExt} file extension`);
  }

  // Download image from S3
  const s3Image = await s3.
    getObject({
      Bucket: bucket.name,
      Key: `${origin}${fullFileName}`
    })
    .promise();

  function gmToBuffer(data) {
    return new Promise((resolve, reject) => {
      data.stream((err, stdout, stderr) => {
        if (err) { return reject(err) }
        const chunks = []
        stdout.on('data', (chunk) => { chunks.push(chunk) })
        stdout.once('end', () => { resolve(Buffer.concat(chunks)) })
        stderr.once('data', (data) => { reject(String(data)) })
      })
    })
  }

  function getStream(body, size, quality) {
    const data = gm(body)
      .resize(size)
      .quality(quality);

    return gmToBuffer(data);
  }

  // use null to optimize image without resizing
  const sizes = [null, 1200, 640, 420];

  // Uploades all images to S3
  const uploadPromises = sizes.map(async size => {
    // Optimize image with current size
    const readableStream = await getStream(s3Image.Body, size, 60);
    const key = size
      ? `${dest}${fileName}_thumb_${size}.${fileExt}`
      : `${dest}${fileName}_original.${fileExt}`;

    return s3.putObject({
      Bucket: bucket.name,
      Key: key,
      Body: readableStream,
    }).promise();
  });

  await Promise.all(uploadPromises);

  cb(null, 'finished');
};

To execute in local I added to the end the function execution:

exports.handler(require('./event.json'), null, (err, res) => {
  console.log(res);
});

I'm using node v10.x as runtime both in local and in AWS.

These are the outputs:

Local:

START RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72 Version: $LATEST
2019-08-09T16:48:24.654Z  52fdfc07-2182-154f-163f-5f0f9a621d72  INFO    finished
END RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72
REPORT RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72  Duration: 1228.68 ms    Billed Duration: 1300 ms        Memory Size: 128 MB     Max Memory Used: 61 MB

AWS:

START RequestId: b8bd8a36-f56b-4732-b9eb-a03fa6c5f75b Version: $LATEST
2019-08-09T16:31:32.994Z	b8bd8a36-f56b-4732-b9eb-a03fa6c5f75b	INFO	finished
END RequestId: b8bd8a36-f56b-4732-b9eb-a03fa6c5f75b
REPORT RequestId: b8bd8a36-f56b-4732-b9eb-a03fa6c5f75b	Duration: 818.42 ms	Billed Duration: 900 ms Memory Size: 256 MB	Max Memory Used: 101 MB	

So, I do not think it is an issue related to memory, but still I'm using 256 MB of RAM and 15s as timeout.

rtalexk avatar Aug 09 '19 16:08 rtalexk

@rtalexk

Set Node to nodejs8.10, set imageMagick=true.

Try that in AWS.

VictorioBerra avatar Aug 09 '19 17:08 VictorioBerra

@rtalexk Also maybe dont use that guys gmToBuffer() thing. Try to use the built in GM toBuffer and callback. I know its not a promise but try eliminating all that extra fluff. And like I said above, most of all, Set Node to nodejs8.10

VictorioBerra avatar Aug 09 '19 17:08 VictorioBerra

I changed te runtime to 8.10 and I also changed the code to not use gmToBuffer function (I don't think it affect anything because it's only a wrapper to convert .stream method to be promise-based) but still the same.

Running the function directly with node (node index.js) works as it should. But once I upload the code to AWS it starts generating images with 0 B of size. The same happens while running with SAM CLI.

It's weird because the only difference is where the code runs. Input, code and libraries (with versions) are the same. I'm using the AWS official docs as reference for this exercise: https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example.html

rtalexk avatar Aug 11 '19 20:08 rtalexk

Oh, I see. Something is wrong with GM. What else could I use to resize/optimize images? I was looking to imagemin with imagemin-pngquant and imagemin-mozjpeg for support to png and jpg/jpeg but it also have problems while running in AWS Lambda. I tried different workarounds but always there's a complex solution that add complex to your system.

Right now I'm looking into Jimp. It looks promising.

rtalexk avatar Aug 12 '19 14:08 rtalexk

I implemented Jimp and everything is working.

The only inconvenient is how much it takes to optimize images:

REPORT RequestId: ...	Duration: 14629.42 ms	Billed Duration: 14700 ms Memory Size: 512 MB	Max Memory Used: 359 MB	

It was tested using the following image:

Dimensions: 2048x1365
Size: 250 KB

It generates 4 new images:

  Original Image 1 Image 2 Image 3 Image 4
Dimensions (px) 2048x1365 2048x1365 1200x800 640x427 420x280
Size (KB)  250  176  66  25  12

I'm worried about how much it is going to cost with larger images and hundreds/thousands of requests.

If you're interested in code, it is available at https://github.com/rtalexk/lambda-image-optimization

rtalexk avatar Aug 12 '19 18:08 rtalexk

For users stringling to make AWS Lambda & Node.js 10 & ImageMagick work

I was able to solve the issue with the following resolution : https://github.com/aheckmann/gm/issues/752#issuecomment-545397275

lblo avatar Oct 23 '19 11:10 lblo