Just some stuff about front-end development

Back

When all hope fails

For a project of mine, I needed to implement a live reload mechanism for an HTML file. The problem was, it was a local file, which of course was a big no-no for every type of fetching data from another location.

Just so we are on the same page, by local file I mean every HTML document that is opened from a location that has a file:// protocol.

The mechanism was supposed to work as follows:

  1. My tool would re-create the HTML file and update a JSON file every time that happened.
  2. The HTML document, if previously opened in the browser would in some magical way read the JSON data from disk and check the content. If the content had a property with newer date than the one that the document already possessed, then the document would reload itself.
  3. Boom, magic.

So, the JSON file is a no-go, because there is no possible way of loading the content of it into the browser and reading it, without coming across the Cross-Origin-Policy. My second idea was to encode the data as a bitmap and save it in a PNG file instead. Now, after that, the browser would simply load the image and decode the data by drawing the image on a CANVAS context.

Did it work? Nope. I forgot, that you cannot actually draw on CANVAS context an image, which was loaded from a different domain (location), and then try to read the pixel data from the CANVAS context. Any attempt to read pixel data from this CANVAS element, would result in such an error:

Unable to get image data from canvas because the canvas has been tainted by cross-origin data.

The process would work perfectly in a different situation (with a server, not a local file), but for me this approach was useless. For those who would actuly want to know how to encode a text message in a bitmap, here is an example. The Node.js script, which creates the PNG file looks like this:

// Run "npm install png" to install
// the required module
var PNG = require('png').Png;
var fs = require('fs');

// We only need 6 pixels, because every pixel
// consists of RGB data, which gives us an
// array with length of 18 (6*3).
var w = 6, h = 1;
var length = w * 3 * h

// The information we want to encode will
// be a timestamp.
var timestamp = Date.now().toString();

// The buffer that will hold the image data.
var buffer = new Buffer( length );

// Lets fill it with a maximum value for every item,
// so we can filter out empty items when reading the
// image data. We will save the timestamp as a string,
// which will constists only of numbers from 0-9,
// so we can assume, that every field with value 255 is
// an empty field. Also, the alpha channel of the PNG
// fill has a value of 255.
buffer.fill(255);

// Lets put our timestamp inside the buffer.
var value;
for( var i = 0; i < timestamp.length; ++i ) {
    buffer[i] = timestamp[i];
}

// Create a PNG file from the buffer...
var png = new PNG(buffer, w, h, 'rgb');

// And save it to the file system...
// var data = png.encodeSync().toString('binary');
// fs.writeFileSync('data.png', data, 'binary');
png.encode(function( image_data ) {
    fs.writeFile('data.png', image_data, function(){
        console.log('Timestamp: %s', timestamp);
    });
});

The timestamp is encoded as a bitmap and saved to the file system as a PNG file, which, after zooming in, looks like this:

Zoomed PNG file

The file, that would load the image and decode it, would be built similarly to this example:

<!DOCTYPE html>
<script>
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');
    var image = new Image();

    function decode( image_data ){
        var info = [], pixel, value;
        for ( var i = 0; i < image_data.data.length; ++i ) {
            pixel = image_data.data[i];
            if ( pixel !== 255 ) {
                info.push( pixel );
            }
        }
        return +info.join('');
    }

    image.addEventListener('load', function() {
        var w = this.width,
            h = this.height;

        canvas.width = w;
        canvas.height = h;

        ctx.drawImage( image, 0, 0 );

        var image_data = ctx.getImageData( 0, 0, w, h );

        var info = decode( image_data );
        console.log('Decoded information: %d', info );
    });
    image.src = 'data.png';
</script>

The solution to my problem was a simple one and hidden just under my nose. The information I needed, wasn't the exact timestamp, only a delta, or just a simple information that the current state is different from the previous one, this way I could inform the HTML file that loaded the image file that something has changed and therefore I can execute a corresponding action (for example, reload the HTML file). The only information I could read from the Image file that was now restricted in this scenario, was the dimension of the file.

Here is an example of Node.js script, that changes the width of a PNG file. On the client side, (an HTML file server under a local file:// URI ) there is a loop that reads this PNG file, and checks if its width has changed, if yes, then is can perform an action, or just try again and read the PNG file again.

The Node.js script:

var PNG = require('png').Png;
var fs = require('fs');

var TIMEOUT_MS = 5000;
var MAX_STATE = 16;

var state = 1;

(function loop(){
    if ( state >= MAX_STATE ) {
        state = 1;
    }

    var w = state, h = 1;
    var length = w * 3 * h;

    var buffer = new Buffer( length );
    buffer.fill(0);

    var png = new PNG(buffer, w, h, 'rgb');

    png.encode(function( image_data ) {
        fs.writeFile('state.png', image_data, function(){
            console.log('State changed to: %s', state);
            setTimeout( loop, TIMEOUT_MS );
        });
    });

    state++;
})();

The HTML client:

<!DOCTYPE html>
<script>
    var TIMEOUT_MS = 1000;

    var current_state = 1,
        state = 1, image;

    ;(function loop(){
        image = new Image(),
        image.addEventListener('load', function() {
            state = this.width;

            if ( state !== current_state ) {
                current_state = state;
                document.write('State changed: ' + state + '!!!<br/>' );
            } else {
                document.write('Checking again...<br/>');
            }

            setTimeout( loop, TIMEOUT_MS );
        });

        // Adding a parameter with timestamp,
        // just to avoid image caching.
        image.src = 'state.png?v=' + Date.now();
    })();
</script>

Of course we need to set a maximal value for the image width, because the value doesn't matter in this case, only that a change occured. Keeping the maximal value small is a good idea.

Using size of an image, to encode information isn't a new idea. Many years ago I've read an article, which described a similar technique. But right now, I cannot find anything about it. I don't suppose that my memmory is playing tricks on me, just that this technique isn't much needed today anymore. I think, it was looong before HTML5 and modern browsers. Microsoft Frontpage was the perfect tool for creating web pages and the MARQUEE was the most perfect HTML tag that ever existed.

Old or not, it's quite handy, if there is just no other way of bypasing browser security policy without telling a user to gut his browser open and turn the security settings off.

comments powered by Disqus