5

I'm trying to duotone an image and plaster it onto a canvas.

Works in desktop browsers:

  • Chrome
  • Firefox
  • Safari
  • Internet Explorer

Fails in mobile browsers:

  • Android

Workable demo on JSFiddle, this example works in Chrome but fails in Android's default browser.

The code is:

<style>
    body {
        background-color: gray;
    }
</style>

<canvas id="mycanvas" width="64" height="64"></canvas>

<script>
    var image = new Image();
    image.src = 'image.png';

    image.onload = function () { //once the image finishes loading
        var context = document.getElementById("mycanvas").getContext("2d");

        context.drawImage(image, 0, 0);

        var imageData = context.getImageData(0, 0, 64, 64);
        var pixels = imageData.data;
        var numPixels = pixels.length;

        for (var i = 0; i < numPixels; i++) { //for every pixel in the image
            var index = i * 4;
            var average = (pixels[index] + pixels[index + 1] + pixels[index + 2]) / 3;

            pixels[index] = average + 255; //red is increased
            pixels[index + 1] = average; //green
            pixels[index + 2] = average; //blue
            //pixels[index + 3] = pixels[index + 3]; //no changes to alpha
        }

        context.clearRect(0, 0, 64, 64); //clear the image
        context.putImageData(imageData, 0, 0); //places the modified image instead
    }
</script>

The summary is:

  • set the background color to gray so alpha can be observed easier
  • create a canvas 64 by 64
  • load a image of a smiley face on a transparent background
  • draw the image onto the canvas
  • get the image's data
  • for every pixel, make the red stronger
  • replace the altered image on the canvas

The smiley face looks like this (block-quoted so you can tell it's transparent):

However, in comparison with the chrome and android browser,

The background of the android drawing is reddish while the chrome drawing is completely transparent.

So...

  • What happened here?
  • How can I change the code so that the android drawing matches with the chrome drawing?

Note: I already tried if (pixels[index + 3] == 0) continue;, and I'm aware of this, but it won't work for images with varying opacity.

Dave Chen
  • 10,887
  • 8
  • 39
  • 67
  • Have you saved your png in the 24- format? – romaneso Jul 23 '14 at 05:42
  • @romaneso No, it's a 32-bit RGBA. I don't even think 24-bit RGBA is even possible, considering RGBA [requires 32 bits](http://upload.wikimedia.org/wikipedia/commons/0/0e/PixelSamples32bppRGBA.png). – Dave Chen Jul 23 '14 at 05:57
  • Oh, thats correct. Try it as a gif maybe – romaneso Jul 23 '14 at 05:58
  • @romaneso Seems to have the same result with a PNG. The background on chrome is completely transparent while the background on my android device is tinted red. – Dave Chen Jul 23 '14 at 06:06
  • i get Uncaught SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data. when im testing your code. Have you been checking for errors contributing to this color-difference? – Marc Guiselin Jul 23 '14 at 09:52
  • @Thouartamazing You have to download the image and place it in your testing area (because cross domain rules apply). As for checking errors, I console.log'd the alphas and they were zero in places that should be zero, which should result in a transparent image, but as seen, does not. – Dave Chen Jul 23 '14 at 09:53
  • I did download your image, and i didnt change your code at all. I just had to rename your image to image.png. Google chrome has... technical difficulties sometimes. it worked in firefox – Marc Guiselin Jul 23 '14 at 09:56
  • 1
    @Thouartamazing It also has to be running on a webserver, making a `.html` file and running it straight with chrome will not work. It also should be on a webserver so you can test it with an android device. – Dave Chen Jul 23 '14 at 09:58
  • ah. try .drawImage() and see the result. It yealded quite a few answers when i had similar problems. http://www.w3schools.com/tags/canvas_drawimage.asp – Marc Guiselin Jul 23 '14 at 10:02
  • @Thouartamazing Sorry I don't understand, I am using `.drawImage()` in the above code. I first draw the image onto the canvas, then I'm changing the image by making red stronger. – Dave Chen Jul 23 '14 at 10:04
  • I wonder if it has anything to do with the way the android browser handles the lossy nature check out [this answer](http://stackoverflow.com/questions/23497925/how-can-i-stop-the-alpha-premultiplication-with-canvas-imagedata/23501676#23501676) for a better explanation of what I mean. – Loktar Jul 24 '14 at 04:19
  • @Loktar Doesn't this only change the colors? In [an example](http://stackoverflow.com/questions/5883220/canvas-putimagedata-color-loss-with-no-low-alpha), it seems only the colors changes, while the alpha works. In this case, I'm not touching the alpha, will this still mess up the alpha? – Dave Chen Jul 24 '14 at 04:43
  • @DaveChen I'm not 100% its really the only thing I can think of though that *could* cause an issue like that besides an outright browser bug :? – Loktar Jul 24 '14 at 14:31
  • @DaveChen I couldn't recreate this, but a suggestion: limit the red to 255 : pixels[index] = 255; or pixels[index] = Math.min(255, average + 255); – yoah Jul 27 '14 at 14:29
  • @yoah It has the same effect even if I set a value directly, `pixels[index] = 120`. Are you saying that when you visit the fiddle on your android, that it is not surrounded by a tinted red background? – Dave Chen Jul 27 '14 at 16:10
  • @DaveChen yes, with fiddle on nexus 1 usinh old Android browser, or on a few new devices with Chrome, I do not get get the tinted background – yoah Jul 28 '14 at 02:06
  • Have you tried gif, but use a background color (most go for pink) then if pink set alpha to 0 this way your forcing it to be set to transparent in the canvas – Barkermn01 Jul 28 '14 at 20:01
  • 1
    Maybe can be related with this issue https://code.google.com/p/android/issues/detail?id=17565 ? – ocirocir Jul 28 '14 at 22:30
  • @MartinBarker I have tried something similar, `if (pixels[index + 3] == 0) continue;` and it works, but as stated in the question above, I need varying opacity. – Dave Chen Jul 29 '14 at 00:00
  • @FedericoReghenzani Looks like exactly what I'm going through. In my experience however, those support forums never actually have any solutions. (Opened in 2011 in this case, and still nothing) I'll probably star it though. – Dave Chen Jul 29 '14 at 00:04

1 Answers1

4

Ok, I have a result.

First of all look at your loop:

for (var i = 0; i < numPixels; i++) { //for every pixel in the image
     var index = i * 4;

index will be more than 3 times bigger than numPixels. And will get undefined values. And average will be NaN. But, then I fixed this - result was the same.

So I tried to use fillStyle and fillRect for every pixel. All becomes fine. My code:

function putImageData(ctx, imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
    var data = imageData.data;
    var height = imageData.height;
    var width = imageData.width;

    dirtyX = dirtyX || 0;
    dirtyY = dirtyY || 0;
    dirtyWidth = dirtyWidth !== undefined ? dirtyWidth : width;
    dirtyHeight = dirtyHeight !== undefined ? dirtyHeight : height;

    var limitBottom = dirtyY + dirtyHeight;
    var limitRight = dirtyX + dirtyWidth;

    for (var y = dirtyY; y < limitBottom; y++) {
        for (var x = dirtyX; x < limitRight; x++) {
            var pos = y * width + x;
            //adding red increased here
            ctx.fillStyle = 'rgba(' + data[pos * 4 + 0] + 255 + ',' + data[pos * 4 + 1] + ',' + data[pos * 4 + 2] + ',' + (data[pos * 4 + 3] / 255) + ')';
            ctx.fillRect(x + dx, y + dy, 1, 1);
        }
    }
};

var image = new Image();
image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gcVBAYmslkm5QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAABc0lEQVR42u2a3QrDMAiFZ9j7v7K7CpTRVtt4NCEnsJt1BP1y4l8nqvrZebXP5osACIAACIAACIAACIAACIAACIAAqpaIlPXkgpwHiIiqqtw5efa8f7csgIgTVVU5AlwGAELOSAjfLKndOWFBQyohVAHWPR/dCwEBCiDCYDSIViH5p/v87xUZZ8IUkJHGEGpoyLu/QjZomffeE+37xwthFH5aKdwNtQyOyhjT1QGzXosWaYBljMdY72laPUZZDLBq+6jTPO41xRXwGB/1Gw5ECOC6StxSASMQ2o5OQ9Jg5VxviYFI5KxgSQBXzmYNPlLaYctw72jr7ncd2DQx4KkCKud/cACeQNghvAmaCDjhCvBCOFODJX2Pgkp7gben+vR5pAqgL0aip8KI2ABNgyMRGzFiT1FARI+eWTDB3w7P6nhKHVBd5pYDGFVABsCU/wccHblLaxUNEwzACvKHB8EVFoeiBEAABEAABEAABEAABEAAe64fre4tZAeLAbcAAAAASUVORK5CYII=';

image.onload = function () {
    var context = document.getElementById("mycanvas").getContext("2d"),
        imageData;

    context.drawImage(image, 0, 0);
    imageData = context.getImageData(0, 0, 64, 64);

    putImageData(context, imageData, 0, 0);
}
Alex
  • 11,115
  • 12
  • 51
  • 64
  • I accepted + bounty because it's essentially not a problem with the code, but a problem with the JavaScript implementation Android browsers use. By utilizing fillRect, it's a much slower, but at least an effective way to apply the same effects onto the image. – Dave Chen Jul 30 '14 at 05:27