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:
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%:
And now the 100%:
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:
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.