1

I am fetching and processing images with nodejs, using node-fetch and canvas. So far, things have been working well. I have a series of image urls, and I fetch them all in parallel using Promise.all:

import { loadImage } from 'canvas';

await Promise.all<CanvasImageSource>(
  urls.map((url: string) => loadImage(url)) // <--- return array of promises
)
  .then((images: CanvasImageSource[]): void => {
     // do some stuff with the images
  })
  .catch((e) => { throw e });

This has been working great. But last night I tried a certain image source url that I want to use, and I'm getting the following error:

Error: read ECONNRESET
    at TLSWrap.onStreamRead (internal/stream_base_commons.js:205:27)
---------------------------------------------
    at TLSSocket.Readable.on (_stream_readable.js:838:35)
    at tickOnSocket (_http_client.js:696:10)
    at onSocketNT (_http_client.js:747:5)
    at processTicksAndRejections (internal/process/task_queues.js:84:21)
---------------------------------------------
    at ClientRequest.onSocket (_http_client.js:735:11)
    at setRequestSocket (_http_agent.js:396:7)
    at handleSocketCreation_Inner (_http_agent.js:389:7)
    at oncreate (_http_agent.js:262:5)
    at Agent.createSocket (_http_agent.js:267:5)
    at Agent.addRequest (_http_agent.js:224:10)
    at new ClientRequest (_http_client.js:296:16)
    at Object.request (https.js:314:10)
    at simpleGet ../../etc

I read How do I debug error ECONNRESET in Node.js? , and the answer there seems to suggest that this is an error from the server side. However, when I print the urls I'm passing to loadImage and then access them in the browser, I am able to get an image back just fine, in the browser. One such url is

https://apps.fs.usda.gov/arcx/rest/services/RDW_Wildfire/ProbabilisticWildfireRisk/MapServer/export?bbox=-13795354.864908611%2C6095394.383573096%2C-13785570.92528811%2C6085610.443952593&size=256%2C256&format=png32&bboxSR=102100&imageSR=102100&f=image&layers=show%3A0

which is a GIS raster image service from the US forestry department. Going that url is my browser returns the image no problem.

I thought that perhaps I might be blasting their server with too many requests at once, as the array of urls usually has 6-10 image urls in it for any given call of the function that runs this code, but I reduced the number of urls to 1 so as to only make 1 request, but no change. Still an error. One thing I noticed when accessing these urls in the browser is that the response is a bit slow. Might that have something to do with it?

A similar (but not the same) government image service url works just fine with this code, retrieving many images in parallel without problem. A sample url for one that works is:

https://landfire.cr.usgs.gov/arcgis/rest/services/Landfire/US_200/MapServer/export?bbox=-13814922.744149616%2C6095394.383573096%2C-13805138.804529114%2C6085610.443952593&size=256%2C256&format=png32&bboxSR=102100&imageSR=102100&f=image&layers=show%3A25

(Edit: the landfire servers are down right now, so that url won't work until they're back up)

I tried using longjohn, as suggested in the other question, to get a more verbose printout of the error, but import 'longjohn' in my code seems to change nothing.

Why would these new urls be throwing this error in node, but not in the browser? I know there are other questions with this subject matter, but they don't seem to helpd me debug my specific issue.

Going further - using the answer

@DipakC wrote a great answer that utilizes axios to fetch images. Using his downloadImage, I am able to fetch images as part of a Promise.all, like so:

await Promise.all(
  tilenames.map((tilename: string) => {
    const url = `${baseurl}/${tilename}.png` // baseurl is remote esri url
    return downloadImage({ url });
  })
);

Ultimately, I need these images to be converted into ImageData so their pixel data can be used. This is easily achieved using some canvas methods. Currently, I use the above code, and then reuse my original code, but this time, instead of using the remote urls for the images, I just use the pathname to the images which I've just downloaded:

// immediately after the Promise.all above:

await Promise.all(
  tilenames.map((tilename: string) => {
    const url = `${localpath}/${tilename}.png` // localpath is where image was downloaded to
    return loadImage(url);
  })
)
  .then((images: Image[]) => {
    const canvas: Canvas = createCanvas(256, 256);
    const ctx: RenderingContext = canvas.getContext('2d');
    ctx.drawImage(image, 0, 0, 256, 256);
    const imageData = ctx.getImageData(0, 0, 256, 256);
    // do something with imageData, more processing
  });

Question: Is there any way to avoid this 2 stage process? Meaning, instead of using downloadImage to download the image as a file, can I write its image data directly to a variable, as I'm doing in the second step with loadImage?

Addendum:

This is the source code for 'loadImage' from the node-canvas src code

function loadImage (src) {
  return new Promise((resolve, reject) => {
    const image = new Image()

    function cleanup () {
      image.onload = null
      image.onerror = null
    }

    image.onload = () => { cleanup(); resolve(image) }
    image.onerror = (err) => { cleanup(); reject(err) }

    image.src = src
  })
}
Seth Lutske
  • 9,154
  • 5
  • 29
  • 78

1 Answers1

2

After reviewed your question, I did some R&D for the source URL which you have added the following is the possible solution to fetch images from the below URL.

Solution 1:

The below code is completely working fine, with the endless requests. I have reviewed the actual usda.gov site and review the headers and responses. I have used the Axios and verify that here the response received in-stream. I might not sure about canvas whether it will handle the stream response or not but, Axios works fine.

The reason to use Axios is that I have also tried with canvas somehow it failed to download an image. After studying the actual application and payload and tried with Axios which works fine and found most compatible.

const axios = require("axios");
const fs = require("fs");
const path = require("path");

/**
 * @description create image directory if not exists
 * @param {String} fPath
 * @returns
 */
async function createDirIfNotExists(fPath) {
  return new Promise((resolve, reject) => {
    if (fs.existsSync(fPath)) {
      console.error("Directory has already created");
      resolve();
    } else {
      fs.mkdirSync(fPath);
      resolve();
    }
  });
}

/**
 * @description download the image
 * @param {Object} requestBody
 * @returns
 */
async function downloadImage(requestBody) {
  const fPath = path.resolve(__dirname, "images");
  await createDirIfNotExists(fPath);
  const writer = fs.createWriteStream(`${fPath}/code.png`);
  const streamResponse = await axios(requestBody).catch((err) => {
    console.error(err);
  });
  streamResponse.data.pipe(writer);
  return new Promise((resolve, reject) => {
    writer.on("finish", resolve("success"));
    writer.on("error", reject("error"));
  });
}

const requestBody = {
  method: "get",
  url: "https://apps.fs.usda.gov/arcx/rest/services/RDW_Wildfire/ProbabilisticWildfireRisk/MapServer/export",
  headers: {},
  params: {
    bbox: "-13795354.864908611,6095394.383573096,-13785570.92528811,6085610.443952593",
    bboxSR: 102100,
    layers: "",
    layerDefs: "",
    size: "256,256",
    imageSR: 102100,
    historicMoment: "",
    format: "png",
    transparent: false,
    dpi: "",
    time: "",
    layerTimeOptions: "",
    dynamicLayers: "",
    gdbVersion: "",
    mapScale: "",
    rotation: "",
    datumTransformations: "",
    layerParameterValues: "",
    mapRangeValues: "",
    layerRangeValues: "",
    f: "image",
  },
  responseType: "stream",
};

(async () => {
  const response = await downloadImage(requestBody).catch(() => {
    console.error("Something went wrong while downloading file");
    return false;
  });
  if (response === "success") {
    console.info("File downloaded succesfully");
  }
})();

Hope my answer will help you to resolve your query. But if you still face the same issue let me know. This code is tested in the local machine as well as repl.it.

====================================================================

Solution 2:

After reviewing your updated description I have also updated my solution and put more efficient solutions in the answer as solution 2.

Solution 2 read the image streamed buffer and convert it into Uint8ClampedArray which might similar format which returns while use ctx.getImageData() function. I have also change responseType to arraybuffer

Hence, this solution resolves both problems listed in the question that you don't need to download the image as well as you received image data Uint8ClampedArray format.

const axios = require("axios");
const fs = require("fs");

/**
 * @description create image directory if not exists
 * @param {String} fPath
 * @returns
 */
async function createDirIfNotExists(fPath) {
  return new Promise((resolve, reject) => {
    if (fs.existsSync(fPath)) {
      console.error("Directory has already created");
      resolve();
    } else {
      fs.mkdirSync(fPath);
      resolve();
    }
  });
}

/**
 * @description get the image data directly from buffer.
 * @param {Object} requestBody
 * @returns
 */
async function getImageData(requestBody) {
  try {
    const { format } = requestBody.params;
    const arrFormat = {
      png: "image/png",
      jpg: "image/jpeg",
      jpeg: "image/jpeg",
      gif: "image/gif",
    };
    const streamResponse = await axios(requestBody).catch((err) => {
      console.error(err);
    });
    // store the streamed data
    const bufferData = streamResponse.data;
    // convert into in base64 format
    const base64Data = `data:${arrFormat[format]};base64,${Buffer.from(
      bufferData
    ).toString("base64")}`;
    // convert into in Uint8ClampedArray (as I referred from the code ctx.getImageData() return Uint8ClampedArray)
    const imageData = new Uint8ClampedArray(bufferData);
    return {
      base64Data,
      imageData,
    };
  } catch (err) {
    console.error(err);
  }
}

const requestBody = {
  method: "get",
  url: "https://apps.fs.usda.gov/arcx/rest/services/RDW_Wildfire/ProbabilisticWildfireRisk/MapServer/export",
  headers: {},
  params: {
    bbox: "-13795354.864908611,6095394.383573096,-13785570.92528811,6085610.443952593",
    bboxSR: 102100,
    layers: "",
    layerDefs: "",
    size: "256,256",
    imageSR: 102100,
    historicMoment: "",
    format: "png",
    transparent: false,
    dpi: "",
    time: "",
    layerTimeOptions: "",
    dynamicLayers: "",
    gdbVersion: "",
    mapScale: "",
    rotation: "",
    datumTransformations: "",
    layerParameterValues: "",
    mapRangeValues: "",
    layerRangeValues: "",
    f: "image",
  },
  // responseType: "stream",
  responseType: "arraybuffer",
};

(async () => {
  const response = await getImageData(requestBody).catch(() => {
    console.error("Something went wrong while downloading file");
    return false;
  });
  console.info("image data received", response);
})();

Hope this solution 2 might resolve your issue.

I suggest using queuing when you want to apply complex image manipulation or process. This might be an efficient way to use your memory and CPU.

Let me know still face an issue.StackOverflow

I open to update my answer if the StackOverflow community has an optimized or alternate solution.

Dipak
  • 2,248
  • 4
  • 22
  • 55
  • Thank you for this answer! I rewrote it into my application with some tweaks, and it is indeed working. I'll award the bounty when SO allows. 2 follow-up questions. **1**: What is the difference between your method and using `loadImage` (I posted the loadImage function in the original question). **2**: I don't need to download the file, but rather read the image into a canvas to extract its imagedata. Is it possible using your method to write the file to a canvas, or in-memory `Image`? That would save me the work of managing the downloaded files that I don't really need... – Seth Lutske May 11 '21 at 22:10
  • ...Currently I am downloading the files using your example, and using my original `Promise.all` code, but replacing the remote urls with my local file paths. That way I can extract the image data. Any suggestions on how to avoid the need to actually *download* the files? – Seth Lutske May 11 '21 at 22:12
  • I told you earlier that I have tried with loadimage method but it does not work. Give me sometime I will refer the loadimage and know You – Dipak May 12 '21 at 02:02
  • When you say extract the data means to get the buffer data and put it into some array or variable? – Dipak May 12 '21 at 02:04
  • If yes then you might get the current stream and converted into buffer. – Dipak May 12 '21 at 02:04
  • Why do you replace the url with your local urls? – Dipak May 12 '21 at 02:05
  • I might be asked so many questions, but to understand your need I should understand the problem statements well. – Dipak May 12 '21 at 02:06
  • Ok I added a section explaining what I mean, in the original question, under ***Going further***. Let me know if that makes sense or if its unclear – Seth Lutske May 12 '21 at 04:01
  • I have seen the loadimage code and seems it might work well with the plain image url. I am not sure but may be this url is like an api which might fetch an image. – Dipak May 12 '21 at 04:10
  • So might be imageurl is not compatible with this type of stuff. – Dipak May 12 '21 at 04:11
  • This is my assumption, to use an imageurl method of canvas – Dipak May 12 '21 at 04:11
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232262/discussion-between-dipakc-and-seth-lutske). – Dipak May 12 '21 at 05:13
  • Recently I have been having some issues with this. For one image source, its working fine, every time - many images are downloaded with no issue. For another image source, *some* of the images download OK, but others are corrupted - meaning they download, but canno't be opened with vscode, or osx preview, or most importantly, cannot be read into the loadImage function - it throws an error that `streamresponse.data` is undefined. This seems to be a recent issue with this image source. Any idea may some images have started downloading as corrupted? Any ideas how to gaurd against that? – Seth Lutske Jul 27 '21 at 00:09
  • I should investigate more on this and then only I will give answer for this. – Dipak Jul 27 '21 at 02:01
  • Thank you! A good example would be trying to use the `downloadImage` function for URL https://apps.fs.usda.gov/arcx/rest/services/RDW_Wildfire/ProbabilisticWildfireRisk/MapServer/export?bbox=-13051775.453750417%2C4070118.8821290666%2C-13041991.514129914%2C4060334.9425085643&size=256%2C256&format=png32&bboxSR=3857&imageSR=3857&f=image. Even when in the browser, when first navigating there, I get a 503, but on refresh, the image is correctly shown in the browser. I guess this is an issue on the server? But how can I 'retry' the downloadImage function until a legimiate image is downloaded? – Seth Lutske Jul 27 '21 at 02:48
  • Could it be an issue that the server only allows a certain # of requests in a certain amount of time from a given host? Is there a way to stagger or delays the requests within the Promise.all so as not to bombard the server? – Seth Lutske Jul 27 '21 at 02:49