Binary data from and to file

Introduction

The main purpose of this article is to show how to handle binary data. There are two applications which use html features related to binary data. The first application reads a bitmap file, parses the binary data to get some meta data and raw image data and to display it on the canvas. The second one applies circle pixelation effect to the browsed or chosen image and allows to save the modified file. In the article titled "Typed arrays and binary data" you can find some basic information about the binary data structures which may help you understand better what happens in the examples described in this article. Thanks to these two articles you can get some solid theoretical and practical knowledge about the subject.

Reading *.bmp file

Using HTML5 features like the File API, data views and typed arrays enables you nowadays to read into an html file any binary file. The attached file readbmp.zip includes the Tizen SDK project which allows to read some bitmap files. You can also run this project in a destkop browser and browse any bitmap file to load but for now, we skipped describing this example.

In the first line we use the resolve method to read the content of the "images" directory contained in the application package. That directory contains two example bitmap files, which you can see on the images above. We use the 'r' parameter because we are only interested in the read operation. The function errorHandler is used across all this project and it's only for logging errors. The fuction resolveHandler is responsible for listing files from a directory. It creates a html list and attaches actions to each element of it, which calls a function to load an image. The function listFiles passes an array with files to it's callback. We iterate through this array of files and create a html list of "li" elements. To each element we add a data attribute with the image path and assign a touch listener. In that listener we call the function loadFile2 with a path parameter.

tizen.filesystem.resolve('wgt-package/images', resolveHandler, errorHandler, 'r');
...
function resolveHandler(dir) {
	dir.listFiles(function(files){
		files.forEach(function(el,i) {
			var li = document.createElement('li');
			li.innerHTML = el.name;
			li.setAttribute('data-file-path','images/'+el.name);
			li.addEventListener('touchstart',function(evt){
				loadFile2(this.getAttribute('data-file-path'));
			});
			document.getElementById('file-list').appendChild(li);
		});
	}, errorHandler);
}

Function loadFile2 creates XMLHttpRequest object which reads a bitmap file and in the load handler passes the binary data as an array buffer to the processRawData function. To get the  binary data as the ArrayBuffer type we set the responseType to an 'arraybuffer'.

function loadFile2(filePath) {
	var req = new XMLHttpRequest();
	req.responseType = 'arraybuffer';
	req.addEventListener('load', function(evt){
		var buffer = evt.target.response;
		processRawData(buffer);
	});
	req.addEventListener('error', errorHandler);
	req.open('GET', filePath, true);
	req.send();
}

The last function is responsible for reading image data and showing them on the canvas element. This function reads in the meta data in the first place . Meta data contains information about the file type, size, position of the image data. First, we create the DataView object on the passed array buffer parameter so we can read the data. Next we read two bytes containing information about the bitmap file type. It's important to get this two letter type to use later the right endianness. Next, we read some necessary meta data to read correctly the pure image data and to create an array for the canvas element. In the two nested loops we fill in the imageData array. Please, see the reference for more details. When the imageData array is filled, we pass it as a parameter to the canvas context of the putImageData method to display the image.

function processRawData(buffer) {
	var dv = new DataView(buffer),
		imgMeta = {};
	imgMeta.bmpType = String.fromCharCode(dv.getUint8(0)) + String.fromCharCode(dv.getUint8(1));
	imgMeta.fileSize = dv.getUint32(2, true)/1024;
	imgMeta.imageDataStart = dv.getUint32(10, true);
				
	imgMeta.headerSize = dv.getUint32(14, true);
	console.log('header size: ' + imgMeta.headerSize);
	if(imgMeta.headerSize!=40) {
		console.log('not supported bmp header');
		return;
	}
		
	imgMeta.imgWidth = dv.getUint32(18, true);
	imgMeta.imgHeight = dv.getUint32(22, true);
	imgMeta.bitsPerPixel = dv.getUint16(28, true);
		
	var rowSize = Math.floor(((imgMeta.bitsPerPixel * imgMeta.imgWidth + 31) / 32)) * 4,
		pixels = new Uint8Array(buffer, imgMeta.imageDataStart),
		cnvs = document.getElementById('bmp-view'),
		ctx = cnvs.getContext('2d'),
		imageData = ctx.createImageData(imgMeta.imgWidth, imgMeta.imgHeight);
		
	cnvs.width = imgMeta.imgWidth;
	cnvs.height = imgMeta.imgHeight;
		
	for (var y = 0, ylen = imgMeta.imgHeight; y < ylen; ++y) { 
		for (var x = 0, bmpIndex, ctxIndex, xlen = imgMeta.imgWidth; x < xlen; ++x) {				 
			bmpIndex = x * 3 + rowSize * y;
			ctxIndex = (x+imgMeta.imgWidth*(imgMeta.imgHeight-y))*4; 
			imageData.data[ctxIndex] = pixels[bmpIndex + 2];
			imageData.data[ctxIndex + 1] = pixels[bmpIndex + 1];
			imageData.data[ctxIndex + 2] = pixels[bmpIndex];
			imageData.data[ctxIndex + 3] = 255;
		} 
	}		
	ctx.putImageData(imageData, 0, 0);
}

Circle pixel effect

This application applies a circle pixel effect to an image. The effect is redrawing any image using small cricles. It is a little bit more complicated than the previous example. It uses the Filesystem API to read and save images but also it utilises a web worker to process the image data. Processing image data can be resources consuming, especially when the image is big. That causes the loss of application responsiveness. To avoid such issues we can move some operations to the web worker. Because of some restrictions related to workers you can't run this project in a desktop browser from a local directory.

There are two ways to load an image. You can browse it from the phone gallery or choose from the list of exmaple images inside the application.  The effect is applied after touching an image.

The following snippet assigns a touch handler to every element of the list. When you touch the list item the loadImage function is called and the path to the image is passed as a parameter.

function listClickHandler(evt) {
	evt.preventDefault();
	ctx.clearRect(0, 0, cnvs.width, cnvs.height);		
	stopWorker();
	loadImage(this.getAttribute('href'));
}
	
var alist = document.querySelectorAll('#file-list a');
for(var i=0; i<alist.length;++i) {
	alist[i].addEventListener('touchstart', listClickHandler);
}

We call the loadFile function because the file input element doesn't return the path to the image, but only a reference to the File object corresponding to it. The event change is triggered only when a different file is browsed. To change this behaviour we assing an empty string to the value property of the file input element.

var fileSelector = document.getElementById('file-selector');
fileSelector.addEventListener('change', function(evt) {
	ctx.clearRect(0, 0, cnvs.width, cnvs.height);		
	stopWorker();		
	loadFile(this.files[0]);
	this.value = '';
});

In both cases, after choosing the picture, a canvas context is cleared and the web worker is stoped.

The function applyEffect does the following things: starts and stops the web worker, sends image data to the worker, shows and hides the busy info and draws cricles on the canvas.

function applyEffect() {
	stopWorker();
	worker = new Worker('js/worker.js');
	worker.addEventListener('message',function(evt){
	var circlesData = evt.data;
	ctx.fillStyle = 'rgba(0,0,0,255)';
	ctx.fillRect(0,0,cnvs.width,cnvs.height);
	for(var i=0; i<circlesData.length ;++i) {
		var o = circlesData[i];
		ctx.fillStyle = 'rgba('+ o.pixData[0]+','+o.pixData[1]+','+o.pixData[2]+',255)';
		ctx.beginPath();
		ctx.arc(o.x, o.y, o.size, 0, 2*Math.PI, false);
		ctx.closePath();
		ctx.fill();				
		}
		stopWorker();
		info.style.visibility = 'hidden';
	});
	info.style.visibility = 'visible';
	var obj = {
		imgW: cnvs.width,
		imgH: cnvs.height,
		orginalData: ctx.getImageData(0, 0, cnvs.width, cnvs.height),
		dotSize: 6,
		dotSize2: 36
	};
	worker.postMessage(obj);
}

The function stopWorker is useful when another picture is chosen during the calculation process. To stop the worker we use the following code:

function stopWorker() {
	if(worker) {
		worker.terminate();
		worker = null;
	}
}

The main task of the script placed in the "worker.js" file is to calculate the position and color of the cricles. After all these calculations the data is sent to the worker's listener using the postMessage (back to the main script file).

self.addEventListener('message', function(evt) {
    
	var obj = evt.data,
		imgW = obj.imgW,
		imgH = obj.imgH,
		orginalData = obj.orginalData,
		dotSize = obj.dotSize,
		dotSize2 = obj.dotSize2,
		circlesData = [];

	for (var y = 0; y < imgH; y+=dotSize) { 
		for (var x = 0; x < imgW; x+=dotSize) {
				var o = {},
				pixData = new Int32Array(3),
				x2, y2;
            
			for(var i=0, dots=0; i<dotSize2 ;++i) {
				x2 = x + i % dotSize;
				y2 = y + Math.floor(i/dotSize);
				if(x2<=imgW && y2<=imgH) {
					++dots;
					var ci = (x2+imgW*(imgH-y2))*4;
					pixData[0] += orginalData.data[ci];
					pixData[1] += orginalData.data[ci+1];
					pixData[2] += orginalData.data[ci+2];
				}
			}

			pixData[0] = Math.floor(pixData[0]/dots);
			pixData[1] = Math.floor(pixData[1]/dots);
			pixData[2] = Math.floor(pixData[2]/dots);

			o.pixData = pixData;
			o.x = x+dotSize/2;
			o.y = imgH-y-dotSize/2;
			o.size = dotSize/2*0.95;
			circlesData.push(o);
		}
	}	
	self.postMessage(circlesData);
});

The last function's goal is to save canvas image to the phone gallery. To achieve this we use a lot of the Filesystem API and one canvas method - toDataURL. It's important to put the Filesystem API methods inside try-catch statements and to log possible errors to have a quick explanation what's wrong.  First, we call the resolve method on the 'images' directory, to get reference to the File object related to that directory. We get this reference (called dir) in a callback function which is passed as the first parameter to resolve. Next, we check if a file exists calling the resolve with our file name. We always save an image using the same file name. If there is no such file yet, we create a new one using the createFile method. When we have a reference to a file object we can write data to it. We open the stream using the openStream method with the first parameter "w". In the second parameter we pass a callback function in which we get reference to the stream object. Finally, we can write data to stream using the writeBase64 method and passing into it the data returned by the canvas toDataURL method. One thing to notice is that we remove the string 'data:image/png;base64,' before writing it.

document.getElementById('save-btn').addEventListener('touchstart', function(evt){
	var fileName = 'circle-pixel-effect.png',
	f;
	try {
		tizen.filesystem.resolve('images', function(dir){
			try {
				f = dir.resolve(fileName);
			} catch(err) {
				console.log(err);
			}
			try {
				if(!f) f = dir.createFile(fileName);
			} catch(err) {
				console.log(err);
			}
				
			if(f) {
				f.openStream('w', function(fs) {
					try {
						fs.writeBase64(cnvs.toDataURL('image/png', 0.8).replace('data:image/png;base64,', '') );
						fs.close();						
					} catch(err) {
						console.log(err);
					}
				}, errorHandler);					
			}
		}, errorHandler, 'w');			
	} catch(err) {
		console.log(err);
		window.open(cnvs.toDataURL('image/jpeg', 0.8),'');
	}
});

Summary

Thanks to the Filesystem API and new JavaScript data types it is possible now to handle binary data. For example, you can read and display files that aren't supported by html tags. You can create applications which can modify, save and display such files. You can move some cpu-heavy operations to web workes, to avoid system lags. Generally, JavaScript gained some powerful features, which allow to make very demanding or desktop-like applications.