As you know, when using the Camera plugin with PhoneGap/Cordova, you have an optional quality setting. It accepts values from 0 to 100 with 50 being the default. I was curious as to how much of an impact this setting had on the final result. Obviously quality can be subjective, but I thought it would be interesting to build a simple tool that would let me test the settings and compare the results.

I began by building a simple Node.js application. Its sole purpose was to simply listen for a form POST and save attached files to a time stamped directory. While not really important to the discussion, I wanted to share the code because this is the first time I've done uploads in Node and Formidable made it freaking easy as heck. Like, I went from thinking I'd need 3-4 hours to figure it out to having the entire server done in about 30 minutes. Anyway, here's the code:

var express = require('express');
var app = express();

var fs = require('fs');

app.use(require('body-parser')());
app.set('port', process.env.PORT || 3000);

var formidable = require('formidable');

app.post('/process', function(req, res) {

	console.log('Attempting process.');
	
	//data directory
	var time = new Date();

	var dir = __dirname + '/output_'+time.getFullYear()+"_"+(time.getMonth()+1)+"_"+time.getDate() + "_"+time.getHours() + "_" + time.getMinutes()+"_"+time.getSeconds();
	fs.existsSync(dir) || fs.mkdirSync(dir);
	console.log("Dir to save is "+dir);
	
	var updir = __dirname + '/uploads';
	fs.existsSync(updir) || fs.mkdirSync(updir);

	var form = new formidable.IncomingForm();
	form.uploadDir = updir;

	form.parse(req, function(err, fields, files) {
		if(err) {
			console.log("error",err);
			res.send("i shit my pants");
		}
		console.log("fields", fields);
		console.log("files", files);
		for(var file in files) {
			if(files[file].name.length) {
				var source = files[file].path;
				var dest = dir + '/' + files[file].name;
				console.log(source, fs.existsSync(source));
				console.log(dest);
				fs.renameSync(source,dest);
				//fs.createReadStream(source).pipe(fs.createWriteStream(dest));
				console.log('copied '+files[file].name);
			}
		}
		res.send('thanks');

	});
});

app.listen(app.get('port'), function() {
	console.log('App started on port '+app.get('port'));
});

Again - don't spend too much time looking this over. It really isn't the important part. Now let's discuss the app. I created a quick Ionic app with one button. The idea would be that you would click the button, and the Camera API will take over. Each iteration of clicking would change to a new quality setting. I decided 20%, 40%, 60%, 80%, and 100% would be good testing points. I also decided to do the same tests for pictures selected from the device. My gut told me that the quality setting would have no impact there, but I wasn't sure. When I tested, I confirmed that quality does not impact existing pictures, so in the code below you will see some parts that are no longer applicable. (And I should stress, this code is an absolute mess. I was going for quick and dirty here, nothing more.)

Another interesting aspect of the code is that I decided to use XHR2 instead of Cordova's File-Transfer plugin. Why? Because the plugin doesn't support multiple uploads at once. With XHR2, I could create a FormData structure with all my file data and send them in one request.

So - the code - and again - this is a mess so don't consider this suitable production code. I'm only including the JavaScript as the HTML is a header and a button.

angular.module('starter', ['ionic'])

.controller("mainController", function($scope) {
	var images = [];
	var idx = 0;
	$scope.currentLabel = {value:""};
	$scope.doingUpload = {value:false};
	
	var doLabel = function() {
		var label = "";
		switch(idx) {
				case 0: label="Camera 20%"; break;
				case 1: label="Camera 40%"; break;
				case 2: label="Camera 60%"; break;
				case 3: label="Camera 80%"; break;
				case 4: label="Camera 100%"; break;
        /*
				case 5: label="Gallery 20%"; break;
				case 6: label="Gallery 40%"; break;
				case 7: label="Gallery 60%"; break;
				case 8: label="Gallery 80%"; break;
				case 9: label="Gallery 100%"; break;
				case 10: label="Upload"; break;
        */
        case 5: label="Upload"; break;
		}
		$scope.currentLabel.value = label;
		if(!$scope.$$phase) {
			$scope.$apply();
		}
	};
	
	//Technically I do pics *and* uploads
	$scope.doPic = function() {
		if(idx <= 4) {
			var options = {destinationType:Camera.DestinationType.NATIVE_URI};
			options.sourceType = (idx<=4)?Camera.PictureSourceType.CAMERA:Camera.PictureSourceType.PHOTOLIBRARY;

			if(idx === 0 || idx === 5) options.quality = 20;
			if(idx === 1 || idx === 6) options.quality = 40;
			if(idx === 2 || idx === 7) options.quality = 60;
			if(idx === 3 || idx === 8) options.quality = 80;
			if(idx === 4 || idx === 9) options.quality = 100;

			navigator.camera.getPicture(function(u) {
				var result = {index:idx, uri:u};
				images.push(result);
				idx++;
				doLabel();
			}, function(e) {
				//if we get an error, might as well die now
				alert(e);
			}, options);
		} else {
			$scope.doingUpload.value = true;
			if(!$scope.$$phase) {
				$scope.$apply();
			}
			console.log('ok, do upload');
      var complete = 0;
			var formData = new FormData();
			images.forEach(function(i) {
				console.log("processing image "+JSON.stringify(i));
				//console.log("test "+decodeURI(i.uri));
				window.resolveLocalFileSystemURL(i.uri, function(fileEntry) {
					fileEntry.file(function(file) {
						var reader = new FileReader();
						reader.onloadend = function(frResult) {
							var data = new Uint8Array(frResult.target.result);
              //using a hard coded name since gallery pics were 'content'
              var fileName = i.index + ".jpg";
							formData.append("index"+i.index, new Blob([data], {type:file.type}), fileName);	
              complete++;
              if(complete === images.length) doUpload(formData);
						};
						reader.readAsArrayBuffer(file);
				   },function(e) {
						console.log("failed to get a file ob");
						console.log(JSON.stringify(e));
					});
				}, function(e) {
					console.log("something went wrong w/ resolveLocalFileSystemURL");	
					console.log(JSON.stringify(e));
				});
			});
      
		}
	};
	
  var doUpload = function(data) {
    console.log('doUpload', data);
    var xhr = new XMLHttpRequest();
    xhr.open('POST', 'http://192.168.1.13:3000/process', true);
    xhr.onload = function(e) {
      $scope.doingUpload.value = true;
      if(!$scope.$$phase) {
        $scope.$apply();
      }
      
    }
    xhr.onerror = function(e) {
      console.log('onerror fire');
      console.dir(e);
    }

    xhr.send(data);
  }
  
	doLabel();
})
.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
})

Ok - so what were the results like? In the following screen shot, 0.jpg represents the first image, which is at 20%, whereas 4.jpg represents the last one, at 100%. Note the file size differences:

shot1

How about the quality? I'll include all the images as an attachment to this blog post, but let's focus on the 20% and 100%. I'm resizing these too of course. First, the 20%:

0

And now the 100%:

4

There seems to be a huge difference in the details of the wallpaper and the colors in the flowers.

Interestingly enough - the 80% is over one meg smaller despite being just 20% less quality. Obviously theres a lot of loss going on - but I think if you look at the 80% - it looks really good:

3

Certainly this isn't an incredibly scientific test. I left my default camera settings on and each click was a new picture. Many things could have impacted the result - how I held the camera - small changes in light - ghosts, etc. For folks curious, I tested this with an HTC M8. If folks want, I can give the iPhone a try as well.

I've uploaded the zip of images here: https://dl.dropboxusercontent.com/u/88185/output_2015_4_27_12_26_22.zip.