I’ve seen Android web browser reduces the quality of images inside an HTML in load time. I’ve reproduced this behaviour with some Android 2.x devices and various emulator configurations. It seems that when it loads 24 or 32 bits images it reduces them to 16bits (4 bits per component: reed, green, blue and alpha). And you cannot do anything about it (if you know how to avoid it, please tell me!).
That doesn’t have very high visual impact. But you may have problems when trying to read data from PNGs (supposed to be lossless). This is a technique based on HTML5 canvas
element and its method getImageData
, storing some data in a PNG file and then reading it from the web using javascript. For example, you can write into a PNG some javascript code – using a gray scale image and storing one character per pixel -. Reading from the web and extracting the original javascript code from its image obtaining a code string can be evaluated with eval
. This is just an example, you can use it to store anything you want: CSS, more HTML, a love letter, etc.
Why would you do this? The answer is PNG compression. But when we try to use this technique in Android browsers, due to that image bit reduction, we only get useless broken data.
Let’s see what happens from a basic example. We’ll generate a gray scale image with only one horizontal line. Each pixel in that line will have values from 0 to 255. The java generation code could be, for example:
// Create image int width = 256; int height = 1; int type = BufferedImage.TYPE_BYTE_GRAY; BufferedImage image = new BufferedImage(width, height, type); // Get raster WritableRaster raster = image.getRaster(); for(int x = 0; x < width; x++) { // and put values raster.setPixel(x, 0, new int[]{x}); } // Write image to disk ImageIO.write(image, "png", new File("image.png"));
That code above simply creates a 8 bit gray scale image and set its pixels with values from 0 to 255 values. Now, from the client side, we could retrieve this data with this javascript code:
// Create image to store PNG var image = new Image(); // When image is load this function is called image.onload = function() { // Create canvas var width = this.width; var height = this.height; var canvas = new Element('canvas', { width:width, height:height }); var context = canvas.getContext('2d'); // Draw image in that canvas... context.drawImage(this, 0, 0); // ...to get its imageData var imageData = context.getImageData(0, 0, width, height); var data = imageData.data; var values = ""; for(var x = 0; x < width; x++) { var value = data[x*4]; values.push(value); } // Do something with values // [...] }; // Set the image src to load it image.src = "route_to/image.png";
Look at canvas
getImageData
method documentation if necessary to understand all the code above.
In all platforms, except Android, you get all the values as expected: values from 0 to 255. But in Android browsers you get something very different. This are the values requested:
0, 0, 0, 0, 0, 0, 8, 0, 8, 8, 8, 8, 8, 8, 16, 8, 16, 8, 16, 16, 16, 16, 24, 16, 24, 16, 24, 24, 24, 24, 33, 24, 33, 24, 33, 24, 33, 33, 33, 33, 41, 33, 41, 33, 41, 41, 41, 41, 49, 41, 49, 41, 49, 49, 49, 49, 57, 49, 57, 49, 57, 57, 57, 57, 66, 57, 66, 57, 66, 66, 66, 66, 66, 66, 74, 66, 74, [... ...], 206, 206, 206, 206, 206, 206, 214, 206, 214, 214, 214, 214, 214, 214, 222, 214, 222, 222, 222, 222, 222, 222, 231, 222, 231, 231, 231, 231, 231, 231, 239, 231, 239, 239, 239, 239, 239, 239, 247, 239, 247, 247, 247, 247, 247, 247, 255, 247, 255, 255, 255
This is like we were lossing the 4 less significant bits. The logic behind it is that Android browser converts all images to 16 bits, 4 bits per component. So, even our image was 8 bits gray scale, it only respects the 4 more significant bits, repeating them for the red, green and blue components.
But you have to be careful, because it also introduce a little dithering while reducing. Let’s see it graphically in this table:
0 | 0 | 0 | 0 | 0 | 0 | 8 | 0 | 8 | 8 | 8 | 8 | 8 | 8 | 16 | 8 | 16 | 8 | 16 | 16 | 16 | 16 | 24 | 16 | 24 | 16 | 24 | 24 | 24 | 24 | ||
33 | 24 | 33 | 24 | 33 | 24 | 33 | 33 | 33 | 33 | 41 | 33 | 41 | 33 | 41 | 41 | 41 | 41 | 49 | 41 | 49 | 41 | 49 | 49 | 49 | 49 | 57 | 49 | 57 | 49 | ||
57 | 57 | 57 | 57 | 66 | 57 | 66 | 57 | 66 | 66 | 66 | 66 | 66 | 66 | 74 | 66 | 74 | 74 | 74 | 74 | 74 | 74 | 82 | 74 | 82 | 82 | 82 | 82 | 82 | 82 | ||
90 | 82 | 90 | 90 | 90 | 90 | 90 | 90 | 99 | 90 | 99 | 99 | 99 | 99 | 99 | 99 | 107 | 99 | 107 | 107 | 107 | 107 | 107 | 107 | 115 | 107 | 115 | |||||
115 | 115 | 115 | 115 | 115 | 123 | 115 | 123 | 123 | 123 | 123 | 123 | 123 | 132 | 123 | 132 | 123 | 132 | 132 | 132 | 132 | 140 | 132 | 140 | 132 | 140 | 140 | 140 | 140 | 148 | ||
140 | 148 | 140 | 148 | 148 | 148 | 148 | 156 | 148 | 156 | 148 | 156 | 156 | 156 | 156 | 165 | 156 | 165 | 156 | 165 | 156 | 165 | 165 | 165 | 165 | 173 | 165 | 173 | 165 | 173 | 173 | 173 |
173 | 181 | 173 | 181 | 173 | 181 | 181 | 181 | 181 | 189 | 181 | 189 | 181 | 189 | 189 | 189 | 189 | 189 | 189 | 198 | 189 | 198 | 198 | 198 | 198 | 198 | 198 | 206 | 198 | 206 | ||
206 | 206 | 206 | 206 | 206 | 214 | 206 | 214 | 214 | 214 | 214 | 214 | 214 | 222 | 214 | 222 | 222 | 222 | 222 | 222 | 222 | 231 | 222 | 231 | 231 | 231 | 231 | 231 | 231 | 239 | ||
231 | 206 | 206 | 206 | 214 | 206 | 214 | 214 | 214 | 214 | 214 | 214 | 222 | 214 | 222 | 222 | 222 | 222 | 222 | 222 | 231 | 222 | 231 | 231 | 231 | 231 | 231 | 231 | 239 | 231 |
So if we want to use the PNG data extraction method and doing it in a fully portable way (or at least working on Android) this method is not valid, because our data will be broken!
In the second part of this article we will discuss one relatively easy solution to this problem. Stay tunned.