Sunday, August 30, 2015

Building a Profile Photo upload with Angular, Node/Express, Cloudinary, and Async.js

Recently I have been spending a lot of time exploring the awesome things about Node.js. The community and user contributions have been tremendous, so tremendous that I rarely find myself having trouble solving a particular problem (there is an existing NPM module out there for everything it seems). However, when approached with writing a user profile/photo upload, there was a lot of different options to weigh. Should I provide the user with some ability to crop the image like many sites do these days? Do I automate image manipulation on my server using node-imagemagick when I want to edit the uploaded file?  What kinds of options work on mobile browsers/touch devices? How do I make it support drag and drop on desktop browsers with Angular.js?

And afterwards, should I save this photo to my database server using GridFS (or gridfs-stream), or some place in the cloud? Do I read a photo file submitted from a form using Express's multer library (express extension for multipart forms), or should I submit the photo as a base64 encoded string? All questions I mulled over extensively when considering this functionality.

After analyzing several options, I have arrived at a very clever solution which I would like to share with the community (as always, it may not fit your use case exactly, but it should give you some things to think about). I can say with confidence now, my solution will do the following:
  • Provide a means to drag and drop photos on modern desktop browsers or select them on mobile devices, using ng-file-upload (lightweight and provides a FileReader shim if you want to support older browsers)
  • Give the user the ability to crop the photo once it is uploaded. For this I used ngImgCrop, for its ease of use and touch support. It also outputs a base64 encoded image that is cropped for you (meaning you won't have to worry about this on the server). 
  • Provide an express server route for handling the image and convert it from a base64 string back to an image.
  • Store the image using Cloudinary, an inexpensive cloud image host that provides image manipulation and fast delivery.
  • Write the code in a generic way that makes it easily reusable and modular, following best Node.js patterns
This is quite a powerful combination when you think about it! After reviewing several blogs about how Grid FS performance and doing image manipulation yourself can bog down the server if not handled extremely carefully, and how storing base64 strings heavily bloats your database, this made the cloud solution an easy choice for me (although if you absolutely must store the image in-house there are two good references here and here).

You can see the result of my work in action when you select your profile photo at urbanchampsports.com.

Now for the code. 

First the view template... (note I have chosen to use Angular material flex box containers for making it responsive, but any responsive framework should work fine).
   
Drop image or click to upload profile photo
Save Photo Change Photo
This defines the following elements for our view:
  • a drop box container for drag and drop of photos
  • the user's current photo (if one has already been saved)
  • a progress indicator (to display during file upload)
  • an image crop container (for use while the user is cropping the photo)
  • and finally, buttons for saving the photo and resetting it (once a photo exists)
Pretty straightforward, but things to note are use of the "ngf-change" directive from the ng-file-upload library, and the "img-crop" directive from the ngImgCrop library. These directives allow you to read the file (giving an accept parameter to determine what type of photo you'd like to accept), and allow the user to crop the image. When the user has finished cropping the image, the "result-image" parameter allows you to specify the scope variable for your resulting image (note the size is used to specify a target size). Here is the relevant scope variables in my Angular controller for handling this interaction:

    /**
     * Profile Photo Upload scope variables
     */
    $scope.loading = false;
    $scope.uploadPhoto = null;
    $scope.croppedPhoto = null;

    $scope.readFileImg = function(files){
      $scope.uploadPhoto = null;
      $scope.croppedPhoto = null;
      $scope.user.photo = null;

      if (files && files.length) {
        var readImgCallback = function(err, img){
          $scope.loading = false;
          if(err) return Toaster.toastErrorMessage($scope, err);

          $scope.$apply(function(){
            $scope.uploadPhoto = img;
          });
        };
        $scope.loading = true;

        Formulator.readImageFile(files[0], readImgCallback);
      }
    };

    $scope.upload = function () {
      if ($scope.croppedPhoto) {
        $scope.loading = true;

        var uploadCallback = function(currentUser){
          currentUser.$promise.then(function(user){
            $scope.user = user;
            Toaster.toastSuccess('Photo saved.');
          });
        };

        Auth.updateProfilePhoto($scope.croppedPhoto, uploadCallback)
          .catch( function(err) {
            Toaster.toastErrorMessage($scope, 'Error saving photo.');
          })
          .finally(function(){
            $scope.loading = false;
          });
      }
      else {
        $scope.loading = false;
      }
    };
To make this solution more generic (so it can be used across my site), I have defined factory utilities for reading the image file (using an HTML5 FileReader), and uploading it to the server (using my Auth factory). Note that the factory below checks the file size does not exceed 4 MB, and the file type is an image. Upon upload completion, we load this image into our ngImgCrop directive (at this point it is a "File" type). Once the user has finished cropping, we upload the output base64 encoded image string to the server (shown below). Note: you can disregard the "Toaster" calls, these are utility calls for notifying the user via Angular Material toasts...

angular.module('urbanChampSportsApp')
  .factory('Formulator', function Formulator() {

    return {
      readImageFile: function(file, cb){
        if(window.FileReader){
          if(file.size > 4000000){
            return cb('Error, photo exceeds max size limit.');
          }
          if(!file.type.match('image.*')){
           return cb('Error, file must be a photo.');
          }

          var reader = new FileReader();
          reader.onloadend = function (event) {
            if(event.target.error != null){
              return cb('Error, please try another photo.');
            }
            else {
              return cb(null,reader.result);
            }
          };
          reader.readAsDataURL(file);
        }
        else {
          return cb("Sorry, this browser doesn't support photo uploads.");
        }
      }
    };
  });

And the upload to the server using $resource...
angular.module('urbanChampSportsApp')
  .factory('Auth', function Auth($location, $rootScope, $http, User, $cookieStore, $q) {
    var currentUser = {};
    if($cookieStore.get('token')) {
      currentUser = User.get();
    }

      /**
       * Update large profile photo
       */
      updateProfilePhoto: function(photo, callback){
        var cb = callback || angular.noop;

        return User.updateProfilePhoto({id : currentUser._id}, {
          photo: photo
        }, function(user) {
          currentUser = User.get();
          return cb(currentUser);
        }, function(err) {
          currentUser = User.get();
          return cb(err);
        }).$promise;
      }
    };
  });

angular.module('urbanChampSportsApp')
  .factory('User', function ($resource) {
    return $resource('/api/users/:id/:controller', {
      id: '@_id'
    },
    {
      get: {
        method: 'GET',
        params: {
          id:'me'
        }
      },
      updateProfilePhoto: {
        method: 'PUT',
          params: {
            controller: 'profilePhoto'
          }
      }
   });
 });

NOTE: There is one gotcha with the ngImgCrop implementation. It currently does not support some portrait (vertical) images uploaded on the iPhone. To fix this, you can apply the following patch to its source:
https://github.com/iblank/ngImgCrop/pull/1

Now for the fun part, handling the image on the Node.js server. The server now takes the following steps:
  • Decodes the base64 image string to a file buffer (stripping any 'data:image' tags)
  • Writes the buffer to a temporary file (so that it can be uploaded to the cloud)
  • Uploads the image to Cloudinary (cloud image host)
  • Removes the temporary file and stores the Cloudinary response url as the user's profile image
  • Returns the response url to the client

First the Express route definition...

'use strict';
var express = require('express');
var controller = require('./user.controller');
var config = require('../../config/environment');
var auth = require('../../auth/auth.service');

var router = express.Router();

router.delete('/:id', auth.hasRole('admin'), controller.destroy);
router.get('/me', auth.isAuthenticated(), controller.me);
router.put('/:id/profilePhoto', auth.isAuthenticated(), controller.updateProfilePhoto);
router.post('/', controller.create);

module.exports = router;


Next we implement the route handler (controller method) for uploading the photo:

var User = require('./user.model');
var passport = require('passport');
var config = require('../../config/environment');
var imageHelper = require('../../components/helper/imageHelper');

/**
 * Updates user profile photo
 */
exports.updateProfilePhoto = function(req, res) {
  var userId = req.user._id;

  User.findById(userId, function (err, user) {
    if(user && req.body.photo) {
      imageHelper.uploadBase64Image('./.tmp/' +userId + '_profile.jpg', req.body.photo, function(err, result){
        if(err) res.send(400, err);
        else{
          user.photo = String(result.url);
          user.save(function(err) {
            if(err) return validationError(res, err);
            res.send(200);
          });
        }
      });
    } else {
      res.send(400);
    }
  });
};

To make our solution more reusable, we are going to define our image decoding and cloud upload modules separately. This keeps our controller clean and allows for code reuse. When the upload is finished, we simply set the url string on our mongoose model for the user, and return it to the client.

The implementation for our "imageHelper" is defined below. It makes use of the awesome Async.js library for handling the asynchronous events we need to process. I highly recommend this utility to every Node.js developer.

//imageHelper.js
'use strict';
var async = require('async');
var fs = require('fs');
var cloudinaryHelper = require('./cloudinaryHelper');

//Decodes a base64 encoded image and uploads it to Cloudinary
module.exports.uploadBase64Image = function(tmpPath, base64Image, cb){
  var imageBuffer = decodeBase64Image(base64Image);

  async.waterfall([
      //write image to tmp disk
      function writeImage(callback) {
        fs.writeFile(tmpPath, imageBuffer.data, function(err) {
          callback(err,tmpPath);
        });
      },
      //upload to cloudinary
      function upload(tmpPath, callback){
        cloudinaryHelper.upload(tmpPath, callback);
      },
      function removeFile(result, callback){
        fs.unlink(tmpPath, function(err) {
          callback(err, result);
        });
      }
    ], function(err, result){
      if(err) console.error(err);
      cb(err,result);
    }
  );
};

function decodeBase64Image (dataString){
  var matches = dataString.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/),
    response = {};

  if (matches.length !== 3) {
    return new Error('Invalid input string');
  }

  response.type = matches[1];
  response.data = new Buffer(matches[2], 'base64');
  return response;
}

For further isolation, we define our Cloudinary upload method separately in a "cloudinaryHelper" module. Note our Cloudinary config constants/app secrets are defined in the global config and not in the module.

//cloudinaryHelper.js
var cloudinary = require('cloudinary');
var config = require('../../config/environment');

cloudinary.config({
  cloud_name: config.cloudinary.cloud_name,
  api_key: config.cloudinary.apikey,
  api_secret: config.cloudinary.apisecret
});

module.exports.upload = function(imgPath, callback){
  cloudinary.uploader.upload(imgPath, function(result) {
    console.log('Cloudinary photo uploaded result:');
    console.log(result);
    if(result){
      callback(null, result);
    }
    else {
      callback('Error uploading to cloudinary');
    }
  });
};


And wah lah! We now have a fully functioning profile photo upload feature! The image is stored in the cloud (as to lighten the load on our app server), and we simply store its hosting URL in the database.

We can also now manipulate its size on the fly! This is a win win.

For example, the following photo upload that is 250px X 250px:
http://res.cloudinary.com/urbanchampsports-com/image/upload/v1440950445/opmsrfg69tip8ryswjdn.png



Can easily be changed to the following (to modify its width and height to 100px X 100px) upon request:
http://res.cloudinary.com/urbanchampsports-com/image/upload/w_100,h_100/v1440950445/opmsrfg69tip8ryswjdn.png

 


Boom! This makes our life much easier on the client, as simply modifying a URL string gives us the image size we'll need for different breakpoints and use cases.

I hope you enjoyed this solution as much as I did. Happy Profiling :)

Tuesday, January 13, 2015

So you want a dashboard?

I recently completed a project for Blue Cross Blue Shield of North Carolina in which we designed and developed a member engagement dashboard, called BlueConnect. Dashboards these days have quite a few features such as draggable tiles, responsive media queries for tablet and mobile devices, animated charts and graphics, third party API services, and maybe even social media integration. I have been swamped with work supporting a project such as this, but I wanted to take some time to jot down some notes and share some insights.

I won't get into the details on everything, but I will just share a few high level concepts. First, some general advice on code structure/team organization.

  • Make sure to leverage a version control system and agree on it first as a team. I highly recommend Vincent Driessen's git flow documented here.
  • Use a build/automation system like Grunt or Gulp. It will save you time at every step of the way and make your code optimized.
  • Choose a client side framework that is robust, and ideally supports testing. We went with Angular because of the widespread community support. Some Angular JS libraries we found useful:
  • For structuring your CSS, I highly recommend splitting your media queries into separate files so that they can be conditionally loaded, and modified for IE. We chose to organize these by breakpoint, and this helped tremendously with code organization.
Now, for the dashboard itself. 

  • First, decide on base tile sizes. Handling a multitude of asynchronous requests in a tile dashboard can be a nightmare if you don't have proper templating and a sound structural approach. For this reason, we first developed base templates upon which our actual content would be loaded, and that could be used for early DOM rendering and tile placement. This can easily be done using Angular's ng-repeat and ng-include directives. These base templates were constrained to "single-wide" and "double-wide" tile for our use case, but these could easily be expanded to as many variations as you need. I recommend choosing sizes that flow easily across different screens for best results, and if you want to meet smaller mobile devices like the iPhone 4, choose a tile size under 320px. Another nice thing about Angular's two way binding/ng-repeat, is that it will automatically re-render tiles quickly if you programmatically change the list (say from outside events, or a change in viewport size).
  • Choose a bin-packing library. These are useful for arranging items within a space in the most efficient way. There are a few of these out there, but the best one I've seen by far is Packery JS. These make your dashboard elements fit nicely, and provide support for things like draggability. There is some great examples of this with Angular here and here and here.
  • After the base content has loaded and you have initialized Packery, you'll want to render your content within your base template (possibly using ng-include or other Angular mechanisms). Ng-include supports variable templates, so we simply passed the name of our content templates in our tile array object. I recommend showing the user a loading spinner for any asynchronous loaded content (such as social media or third party feeds) in your base template. You can do this easily using Angular-Spinner and libraries such as imagesLoaded for any longer loading graphics. 
Lastly, consider animations for your app. These may require some special handling, but for simple animations, you can make use of Animate.css. We chose to add a delayed "fade In" effect for the tiles, which nicely presented them to the user as their content is loaded. Another useful library for animating graphs is Highcharts.js. This library certainly gives your application a dashboard "feel" as your user is presented with animated data charts.

There you have it...a dashboard is waiting ahead! If you have any trouble or questions, please feel free to contact me.