Testing Camera Quality Settings and PhoneGap/Cordova

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.

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also