Allowing user upload their images to web server is quick common in modern web app, like changing account avatar, attaching blog pictures or posting image tweets. The images from user side can be difference, they have different qualities, sizes and types. Regarding saving those images directly into our database or filesystem (thinking about saving an 10MB image into database), we mostly like to convert them into our standards first. That is the reason we need server side image pre-processing.
Why image pre-processing really matters in web application
Photographing device improved dramatically in recent years. Not to mention SLR camera, High quality image can be achieved by using mobile device or webcam. In spite of data compression, high resolution images could just mean high disk capacity. A 120-megapixels image can be larger than 20MB. That would be painful to store such a huge buffer data into database, and that is even worse if web app run on tight resources. Meanwhile, after uploading images, the 'look' of those images may not meet requirements, for example, an user uploaded an 1680x1050 image just for his tiny avatar. All the cases above need us to build a server side middleware to convert an uploaded images to an affordable and usable image.
Build A Simple Node Image-processing Middleware
In this post, we gonna build a simple node application allowing us to process user uploaded images. This application would have following features:
- Allow user upload multiple images to server through our RESTful API
- Resize the uploaded images to 512x512 pixels
- Lower the quality of uploaded images to 10% of original quality
- Encode the uploaded images to base64 string
- Return the base64 string to client side, then allowing user preview their uploaded images
In this application, we have following dependencies:
bluebird
A promise library, we use it to handle asynchronous call and avoid callback pyramidbody-parser
Node.js body parsing middlewareexpress
We use express to build our node applicationjimp
A lightweight, but powerful image processing library, the most important thing is that it has no dependencies, you can use it without any 3rd party software running on your serverjquery
we use jquery to do the ajax calllodash
server side utility librarymaterialize-css
A front-end Google Material Design library, here we use it to make our web page looks more friendlymulter
A Node middleware to handle multipart/form-data, we use it to serialize uploaded imagesyou can copy following json to your
package.json
file, and runnpm install
under your app root path:
{
"name": "upload-img-demo",
"version": "1.0.0",
"main": "app.js",
"license": "MIT",
"dependencies": {
"bluebird": "^2.10.2",
"body-parser": "^1.14.0",
"express": "^4.13.3",
"file-type": "^3.0.0",
"jimp": "^0.2.8",
"jquery": "^2.1.4",
"lodash": "^3.10.1",
"materialize-css": "^0.97.1",
"multer": "^1.0.3"
}
}
Build The App From Scratch
Routing
'use strict';
let express= require('express')
,multer = require('multer')
,upload = multer()
,app = express()
//Import imgProcessor module which we would implement later
,imgProc = require('./imgProcessor');
app.get('/', (req, res, next)=>{
res.sendFile(__dirname+'/main.html');
});
app.post('/uploadImg', upload.array('pics'), (req, res, next)=>{
//Call the convertImgs method and pass the image files as its argument
imgProc.convertImgs(req.files).then((imageStringArray)=>{
//After all image processing finished, send the base64 image string to client
res.json(imageStringArray)
})
});
app.listen(8888, ()=>{
console.log('Hosted on Port 8888')
});
The server side code is pretty straight-forward, the app listen on port 8888, and it only have two routes, GET:/
and POST:/uploadImg
, in first route, server send our main pagemain.html
to client, the latter would response to image uploading request.
Notice, in /uploadImg
we use multer middleware upload.array('pics')
to parse the uploaded files from POST payload, then we can access those files through req.files
Image Processor
Now, let's build imgProcessor
'use strict';
let Jimp = require('jimp')
,fs = require('fs')
,path = require('path')
,_ = require('lodash')
,Promise = require('bluebird')
,fileType = require('file-type');
module.exports = {
convertImgs(files){
let promises = [];
_.forEach(files, (file)=>{
//Create a new promise for each image processing
let promise = new Promise((resolve, reject)=>{
//Resolve image file type
let type = fileType(file.buffer);
//Create a jimp instance for this image
new Jimp(file.buffer, (err, image)=>{
//Resize this image
image.resize(512, 512)
//lower the quality by 90%
.quality(10)
.getBuffer(type.mime, (err, buffer)=>{
//Transfer image file buffer to base64 string
let base64Image = buffer.toString('base64');
let imgSrcString = "data:" + type.mime + ';base64, ' + base64Image;
//Resolve base94 string
resolve(imgSrcString);
});
})
});
promises.push(promise);
});
//Return promise array
return Promise.all(promises);
}
};
ImgProcessor only have one method convertImgs
, this method basically doing three things:
- Create a promise for each image processing period, and push it to a promise array
- Create a JIMP instance for each image, and resize and lower quality of the image by calling JIMP API
- Transfer image file buffer data to base64 string format and resolve the string
After calling this method on a image, a related low-quality and shrinking image would be produced.
View
The last thing left is to build our main page
<html>
<head>
<title>Upload Image Demo</title>
<script src="node_modules/jquery/dist/jquery.min.js"></script>
<script src="node_modules/materialize-css/dist/js/materialize.min.js"></script>
<link rel="stylesheet" href="node_modules/materialize-css/dist/css/materialize.min.css">
</head>
<body class="container">
<div class="row">
<form id="form1" class="col m4 offset-m4" action="/uploadImg" enctype="multipart/form-data" method="post">
<div class="file-field input-field">
<div class="btn">
<span>File</span>
<input type="file" multiple="" accept="image/jpeg,png" name="pics">
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text" placeholder="Upload one or more files">
</div>
</div>
<div class="input-field">
<button type="submit" class="waves-effect waves-light btn">Submit</button>
</div>
<div class="progress" style="display:none">
<div class="indeterminate"></div>
</div>
</form>
</div>
<div class="row img-preview"></div>
<script>
$(function () {
$('#form1').on('submit', function(event){
event.preventDefault();
var data = new FormData(this);
data.append( 'pics', $('input[name="pics"]')[0].files);
$('.progress').css({
display:'block'
});
$.ajax({
type:'POST',
url: $(this).attr('action'),
data:data,
cache:false,
contentType: false,
processData: false,
success:function(data){
for(var i=0;i<data.length;i++){
var template =
'<div class="col m4"><div class="card"><div class="card-image"><img src="'
+ data[i] + '"></div></div></div>';
$('.img-preview').append(template);
}
$('.progress').css({
display:'none'
});
},
error: function(err){
$('.progress').css({
display:'none'
});
}
});
})
})
</script>
</body></html>
In order to enable multer middleware on form posting data, in form element, we set enctype
attribute to multipart/formdata
, and set input element type
to file
, name
to pics
, then we prevent the default submit event, and do AJAX call using jQuery. We use HTML5 FormData to build our POST payload. And on success callback, we create <img>
element for each returned image base64 string, and append it to <body>
as a picture preview. We also add preloader element <div class="progress">
to indicate user that image is under processing.
Performance
Image Processing could be a time consumed job, many algorithms and theories out there try to speed up this process, at this point, we won't drill down to algorithm, we just want to make sure our end user could be notified during image processing, loader icon or progress bar both are great idea to be added on the page.