1

I'm trying to use the canvas module to create a PNG file in a node.js script.

I have a file, image.js that handles the image creation:

const { createCanvas, loadImage } = require('canvas');
const fs = require('fs');

function generateImage(param1, param2) {
  const canvas = createCanvas(4096, 4096);
  const ctx = canvas.getContext('2d');
  // ...
  return new Promise(function (resolve, reject) {
    loadImage('base.png').then((image) => {
      ctx.drawImage(image, 40, 40, 200, 200);

      let out = fs.createWriteStream(__dirname + '/temp.png');
      let stream = canvas.pngStream();

      stream.on('data', function (chunk) {
        out.write(chunk);
      });

      stream.on('end', function () {
        console.log('saved png');
        out.close(); // i probably don't need both, but neither seems to work properly...
        out.end();
        resolve();
      });
    });
  });
}

module.exports = generateImage;

When I append generateImage('a', 'b'); at the end of this file and then run it separately (node image.js), it works as expected and produces my image. However, when I try to call it from a different file (in the same folder), it seems like the file handle does not close, and the image on disk is an empty file while the script is running.

main.js (simplified):

const generateImage = require('./image');
const fs = require('fs');

async function main() {
    var stats = fs.statSync("temp.png");
    console.log(stats["size"]); // output: 0, expected: > 0
}

generateImage("text", "text").then(() => main());

I'm new to node.js, so it's very possible that I'm missing something obvious.

martonbognar
  • 462
  • 3
  • 14
  • Is this the complete code? I think you are missing `steam.pipe(out)` in the fist snippet maybe? – Anand Undavia Jul 19 '18 at 17:24
  • Not related to your problem, but avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! Put the `loadImage('base.png')` call outside the `new Promise`, then chain them with `then`. – Bergi Jul 19 '18 at 17:43
  • 2
    `out.end` [appears to be asynchronous](https://nodejs.org/api/stream.html#stream_writable_end_chunk_encoding_callback). You will need to wait for the `close` or `finish` events on the `out` stream before resolving the promise. – Bergi Jul 19 '18 at 17:48
  • Possible duplicate https://stackoverflow.com/questions/44013020/using-promises-with-streams-in-node-js – Yury Tarabanko Jul 20 '18 at 08:32
  • 2
    @Bergi Thank you, that was the issue! My event listeners now look like the following: `stream.on('end', () => out.end());`, `out.on('close', () => resolve());`. I wasn't able to figure out how exactly I could improve my code to avoid the mentioned antipattern though. If you could post an answer with a more elegant solution, I would be glad to accept it! – martonbognar Jul 20 '18 at 08:36
  • @YuryTarabanko I think my function calls were fine, so I don't think it's related, but thankfully Bergi's solution worked. Thanks for the suggestion though! – martonbognar Jul 20 '18 at 08:37
  • @martonbognar The linked question contains concise solution to your problem. You could use `pipe` instead of manually transfering data between streams. Then listening to `end` event on piped stream would be enough to correctly resolve the promise. – Yury Tarabanko Jul 20 '18 at 08:43
  • 1
    @martonbognar Basically just swap `return new Promise(function (resolve, reject) { loadImage('base.png').then((image) => {` for `return loadImage('base.png').then((image) => new Promise(function (resolve, reject) {`. Or alternatively, if you want to use `async`/`await`, then it's `const image = await loadImage('base.png'); return new Promise(function (resolve, reject) {…});` – Bergi Jul 20 '18 at 15:14
  • out.on('finish', resolve) – Ankit Manchanda Jul 23 '18 at 09:20

1 Answers1

1

According to Bergi's advice, I needed to add another event listener for the out stream's close event, and only call resolve() once that is triggered. I also chained the promises instead of embedding them as in the question.

function generateImage(param1, param2) {
  const canvas = createCanvas(4096, 4096);
  const ctx = canvas.getContext('2d');
  // ...
  return loadImage('base.png').then((image) => new Promise(function (resolve, reject) {
    ctx.drawImage(image, 40, 40, 200, 200);

    let out = fs.createWriteStream(__dirname + '/temp.png');
    let stream = canvas.pngStream();

    stream.on('data', (chunk) => out.write(chunk));
    stream.on('end', () => out.end());
    out.on('close', () => resolve());  // this was missing
  }));
}
martonbognar
  • 462
  • 3
  • 14