Have you done image manipulation on a server?

That was one requirement I had a while back that I didn’t know how to solve at the moment.

We were working with jsPDF to create report PDFs in one of our Ionic apps, but we had an issue, all of our images where stored online, and you can’t pass a url to jsPDF for images.

It only takes base64, data URI, and I think the blob object.

Our first try was to do the base64 transformation client side, and send all those strings to create the PDF. The issue was that if we had more than 10 images it started to lag.

So we decided to move this server-side, and created a Cloud Function that handled it for us.

For that, we need a Cloud Function that:

  • Listens to the database for when we add the regular images URL.
  • Resizes that image so that we don’t store something to heavy.
  • Transforms it to base64.
  • Stores the base64 string in the database.

With that in mind, let’s jump into coding, this article asumes you already have a project and have Cloud Functions initialized, if not, go through this article first.

Installing the packages we need

The first thing we’ll do is to install the packages we need, in our case those are two, we used the request package to fetch the image URL, and the sharp package to resize it.

npm install request sharp

And then, import them both at the top of the functions file

var request = require('request').defaults({
  encoding: null
});
const sharp = require('sharp');

We’re using encoding: null to tell request that you want a Buffer, not a string. We need that buffer to create the base64 string.

Create the Cloud Function

We want to first create the function that listents to the database, with a few checks in place. In our case, we had a projects collection and each document had a before and after picture we needed for the report:

exports.convertImageToPDF = functions.firestore.document('projects/{projectId}').onWrite(async (change, context) => {
  // We get the project's ID from the context param.
  // We get the actual project data from the change param, we want to
  // use the .after to signal is the new data we're listening to.
  const projectId = context.params.projectId;
  const currentProject = change.after.data();
});

Then we want to check, if we are updating anything else on that project and we already have that base64 property, we want to stop the function there.

exports.convertImageToPDF = functions.firestore.document('projects/{projectId}').onWrite(async (change, context) => {
  const projectId = context.params.projectId;
  const currentProject = change.after.data();

  let beforePictureBase64 = currentProject.beforePictureBase64;
  let afterPictureBase64 = currentProject.afterPictureBase64;

  if (beforePictureBase64 && afterPictureBase64) {
    console('Both pictures are here');
    return;
  }
});

Then we want to ask, if there’s no before picture, or no after picture and convert those to base64.

We’ll call a function to do the transformation, we’ll cover that as soon as we’re done with the cloud function that talks to the database.

exports.convertImageToPDF = functions.firestore.document('projects/{projectId}').onWrite(async (change, context) => {
  const projectId = context.params.projectId;
  const currentProject = change.after.data();

  let beforePictureBase64 = currentProject.beforePictureBase64;
  let afterPictureBase64 = currentProject.afterPictureBase64;

  if (beforePictureBase64 && afterPictureBase64) {
    console('Both pictures are here');
    return;
  }

  if (!beforePictureBase64) {
    // currentProject.beforePicture is the online URL of the before picture
    beforePictureBase64 = await toDataURL(currentProject.beforePicture);
  }

  if (!afterPictureBase64) {
    // Same as above but for the after picture
    afterPictureBase64 = await toDataURL(currentProject.afterPicture);
  }

  // If we get to this point and still we have no transformed images,
  // we want to stop the function.
  if (!beforePictureBase64 && !afterPictureBase64) {
    console.log('No pictures here');
    return;
  }

  // And then we want to store the base64 pictures in the same project.

  return admin
    .firestore()
    .doc(`projects/${projectId}`)
    .update({
      beforePictureBase64: beforePictureBase64 || null,
      afterPictureBase64: afterPictureBase64 || null,
    });
});

Now, let’s talk about the toDataURL() function, since all the image transformation is happening there.

First, we need the function to take the online url as a parameter, and return a Promise, since we’re going to be doing asynchronous things.

function toDataURL(url) {
  return new Promise((resolve, reject) => {});
}

Then we’re going to use the request module to fetch that image, if there’s no error and everything went well, we do the image manipulation, if something’s wrong, we throw the error.

function toDataURL(url) {
  return new Promise((resolve, reject) => {
    request.get(url, function (error, response, body) {
      if (!error && response.statusCode === 200) {
        // Do the transformation
      } else {
        throw error;
      }
    });
  });
}

Then, we’re going to resize the original image, remember, a Firestore document has a hard limit of 1MB in size, and a base64 image is usually 30% heavier than the original one.

function toDataURL(url) {
  return new Promise((resolve, reject) => {
    request.get(url, function (error, response, body) {
      if (!error && response.statusCode === 200) {
        return sharp(body)
          .resize(100, 100)
          .toBuffer()
          .then(resizedImage => {
            // Transform to base64
          })
          .catch(err => {
            throw err;
          });
      } else {
        throw error;
      }
    });
  });
}

We’re using the sharp module to resize it, we’re passing the height and width we want for the image, and then making sure that response is also a buffer.

And lastly, we’re forming our base64 string:

function toDataURL(url) {
  return new Promise((resolve, reject) => {
    request.get(url, function (error, response, body) {
      if (!error && response.statusCode === 200) {
        return sharp(body)
          .resize(100, 100)
          .toBuffer()
          .then(resizedImage => {
            data =
              'data:' + response.headers['content-type'] + ';base64,' + new Buffer(resizedImage).toString('base64');

            resolve(data);
            return data;
          })
          .catch(err => {
            throw err;
          });
      } else {
        throw error;
      }
    });
  });
}

And that’s it, the toDataURL(url) function will return a valid base64 string we can then store in Firestore and use for our pdf manipulation :)

Do let me know if you have any issues with this one, it was a major pain for us, we tried several different things before arriving to this.