21

When writing pixels to an HTML Canvas context using putImageData I find that the pixel values are not exactly the same when I fetch them again. I have put up a sample test page showing the problem. Boiled down, the problem is:

var id = someContext.getImageData(0,0,1,1);
id.data[0]=id.data[3]=64; // 25% red, 25% alpha
id.data[1]=id.data[2]=0;  // No blue or green
someContext.putImageData(id,0,0);
var newData = someContext.getImageData(0,0,1,1);
console.log( newData.data[0] );

On Chrome v8, the red value comes back as 63; on Firefox v3.6, Safari v5, and IE9 the red value comes back as 67 (all on Windows). On OS X, Chrome v7, Safari v5, and Firefox v3.6 also come back as 67. None of them come back as the 64 value originally set!

Using setTimeout to delay between setting and re-fetching makes no difference. Changing the background of the page makes no difference. Using save() and restore() on the context (per this unlikely article) makes no difference.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • I've put up another test page showing the [effects of alpha on a set/returned value](http://phrogz.net/tmp/htmlcanvas_putimagedata2.html). The lower the alpha, the more widely-varied the results. Setting `R:5 G:0 B:0 A:1` comes back as `R:255 G:0 B:0 A:1`. – Phrogz Dec 01 '10 at 18:29
  • Were you ever able to get around this issue? – Billy Jul 06 '14 at 03:51
  • @Billy No; even the HTML5 specs have a Note (see the link in my comment below) about the fact that set/get may result in different values. – Phrogz Dec 17 '14 at 15:14

3 Answers3

17

ImageData is defined in HTML5 as being unpremultiplied, but most canvas implementations use a premultiplied backing buffer to speed up compositing, etc. This means that when data is written and then read from the backing buffer it can change.

I would assume that Chrome v8 picked up a buggy version of the [un]premultiplying code from webkit.org (It has been broken before, although i don't recall any recent occurances, and that doesn't explain the windows only variance)

[edit: it could be worth checking a webkit nightly on windows? as the imagedata implementation doesn't have anything platform specific it's shared between all webkit browsers and could simply be broken in MSVC based builds]

olliej
  • 35,755
  • 9
  • 58
  • 55
  • To be clear, all browsers are 'buggy': none of them return `64` after setting the same. Chrome v8b happens to return a different value from the other browsers (albeit closer to the original). I've edited the question to highlight this. – Phrogz Nov 30 '10 at 16:09
  • 2
    Thanks for the comment about the specs wrt premultiplied alpha. The specs [explicitly note this problem](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html), saying: _"Due to the lossy nature of converting to and from premultiplied alpha color values, pixels that have just been set using `putImageData()` might be returned to an equivalent `getImageData()` as different values."_ – Phrogz Nov 30 '10 at 16:15
  • I have a recollection that Opera doesn't get this problem, but maybe they've since changed their implementation. – olliej Dec 01 '10 at 23:06
7

HTML5 specification encourages browser vendors to use something that is called Premultiplied Alpha. In essence this means that pixels are stored in 32-bit integers where each channel contains a 8-bit color value. For performance reasons, the Premultiplied Alpha is used by browsers. What it means is that it premultiplies color values based on the alpha value.

Here's an example. You have a color such that the values for RGB are 128, 64, 67. Now, for the sake of higher performance, the color values will be premultiplied by the alpha value. So, in case the alpha value is 16, all the color values will get multiplied by 16/256 (= 0.0625). In this case, the resulting values for RGB become 8, 4, 4.1875 (rounded to 4 because pixel color values are not floats).

The problem shows up when you do exactly what you are doing here; setting color data with a specific alpha value and then pulling back the actual color values. The previous Blue color of 4.1875 that got rounded to 4 will become 64 instead of 67 when you call getImageData().

That is why you are seeing all this and it will never change unless the underlying implementation in a browser engine changes to use a color system that does not suffer from this.

Tower
  • 98,741
  • 129
  • 357
  • 507
  • 1
    Could you cite where the HTML 5 specs encourage this? I see the specs requiring that all imagedata get/set use **un** premultiplied alpha, and I see the note (that I quoted above) where they say what might occur _if_ the implementation happens to use premultiplied. – Phrogz Dec 16 '10 at 20:47
  • @Phrogz what spec was that? – NoBugs Dec 17 '14 at 07:42
  • @NoBugs The one I linked to in my comment on olliej's answer. – Phrogz Dec 17 '14 at 15:12
4

Looks like a rounding issue to me...

64/255 = 0.2509... (rounded down to give 0.25)
0.25 * 255 = 63.75 (rounded down to give 63)
== OR ==
64/255 = 0.2509... (rounded up to give 0.26)
0.26 * 255 = 66.3  (rounded up to give 67)

Remember that 255 is the maximum value, not 256 ;)

EDIT: Of course, this wouldn't explain why the alpha channel is behaving...

Niet the Dark Absol
  • 320,036
  • 81
  • 464
  • 592
  • Interesting data points. I suppose if there was a `*100` in the underlying code before a `round()` or `ceil()` that might make sense. I would have said that was unlikely, but you've managed to reach the two magic numbers semi-directly. :) – Phrogz Nov 30 '10 at 00:22
  • One odd note, however: if you set the alpha to `255` the red comes back out as `64` as desired. Perhaps there's a premultiply pipeline involved? – Phrogz Nov 30 '10 at 00:24