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
  })
}

1 Answer 1

2
+50

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.

Sign up to request clarification or add additional context in comments.

16 Comments

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...
...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?
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
When you say extract the data means to get the buffer data and put it into some array or variable?
If yes then you might get the current stream and converted into buffer.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.