Premultiplied alpha rounding error with canvas 2D
Categories
(Core :: Graphics: Canvas2D, defect, P3)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox160 | --- | fixed |
People
(Reporter: info, Assigned: python273)
Details
(Keywords: parity-chrome, parity-edge, testcase)
Attachments
(2 files)
User Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0
Steps to reproduce:
I have created a 1x1 px PNG with the values [12, 187, 146, 62], put it into a canvas and read back the values. Here is the code with the base64 encoded PNG (see also https://stackoverflow.com/q/60074569/1796523):
let image = document.createElement('img');
image.addEventListener('load', () => {
let canvas = document.createElement('canvas');
let ctx = canvas.getContext("2d");
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
document.body.innerHTML = data;
});
image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGPg2T3JDgADyAGYmiSbAQAAAABJRU5ErkJggg==";
Actual results:
The code returned [12, 189, 148, 62].
Expected results:
It should return [12, 185, 144, 62] which is based on the premultiplied alpha value of [3, 45, 35, 255]. Firefox seems to calculate [3, 46, 36, 255] (see also https://stackoverflow.com/a/60076433/1796523). This is working correctly in Chrome.
Comment 1•6 years ago
|
||
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0
20200205055433
Comment 2•6 years ago
|
||
Comment 4•6 years ago
|
||
Andrew, anything silly going on in the PNG decoder here?
Updated•3 years ago
|
| Assignee | ||
Comment 5•4 days ago
|
||
Still reproducible in Firefox 157.0 on macOS. A canvas → PNG → canvas round trip changes [127,0,0,8] to [159,0,0,8], while a direct putImageData()/getImageData() round trip preserves it.
On a larger image, repeated round trips caused visible progressive brightening/banding after 5–10 cycles, with alpha unchanged. Chromium 151 and WebKit 26.5 remained stable through 50 cycles. This affects PNG-based storage in image editing applications, especially after repeated round trips.
Minimal reproduction:
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 1;
const ctx = canvas.getContext("2d");
ctx.putImageData(
new ImageData(new Uint8ClampedArray([127, 0, 0, 8]), 1, 1), 0, 0
);
console.log("before:", Array.from(ctx.getImageData(0, 0, 1, 1).data));
const img = new Image();
img.src = canvas.toDataURL("image/png");
await img.decode();
ctx.clearRect(0, 0, 1, 1);
ctx.drawImage(img, 0, 0);
console.log("after:", Array.from(ctx.getImageData(0, 0, 1, 1).data));
Possible code pointers: nsPNGEncoder::ConvertHostARGBRow() unpremultiplies using nearest rounding:
(channel * 255 + alpha / 2) / alpha
However, UnpremultiplyChunkFallback() in Swizzle.cpp uses a fixed-point reciprocal with truncation. PremultiplyChunkFallback() in the same file uses an upward-biased divide-by-255 approximation.
This rounding mismatch appears to explain the reproduction: premultiplied red 4 at alpha 8 reads back as 127, exports as 128, then becomes premultiplied red 5 on import and reads back as 159.
Encoding the saved getImageData() RGBA bytes directly into PNG with Python Pillow, then importing through the same Firefox path, avoided the drift in the tested image.
| Assignee | ||
Comment 6•4 days ago
|
||
Updated•4 days ago
|
Comment 9•4 days ago
|
||
Backed out for causing mochitest gpu failures
Backout link: https://hg.mozilla.org/integration/autoland/rev/a2ab2783e63a734fd22a0db03267a316add6bad5
Comment 10•4 days ago
|
||
Comment 11•4 days ago
|
||
Updated•3 days ago
|
| Assignee | ||
Comment 12•3 days ago
|
||
- Corrected the gradient expectations to pixel-center sampling; stored/rendered
pixels are unchanged, and tolerance remains 3. - Regenerated four JPEG/WebP references and both affected favicon PNG variants
after verifying the intended nearest-rounding pixel changes. - Removed obsolete FAIL expectations for 18 compositing WPTs (including the
offscreen/worker variants). These now genuinely pass; references and fuzzy
limits are unchanged.
The original C++ fix is unchanged. Optimized local checks pass all 27
image/favicons files, the legacy canvas test, all 267 PNG round-trip assertions
on software/verified NVIDIA acceleration, and 212 graphics/image gtests.
Additional opaque-PNG and compositing mismatches found during local C-zlib (USE_LIBZ_RS= test_imgtools.js) and
forced-GPU testing also reproduce on matched prepatch builds, so they appear
unrelated to this change.
Comment 13•3 days ago
|
||
Comment 14•3 days ago
|
||
| Assignee | ||
Comment 16•2 days ago
|
||
Updated D331332 with the GTK snapshot reference fix
Comment 17•2 days ago
|
||
Comment 18•1 day ago
|
||
Comment 19•1 day ago
|
||
Backed out for causing failures at conformance-alpha-premultiplied-reftest.html.
Backout link: https://hg.mozilla.org/integration/autoland/rev/ee203168e091
Comment 20•1 day ago
|
||
Updated•1 day ago
|
| Assignee | ||
Comment 21•1 day ago
|
||
Updated the JPEG XL reftest’s pixel-count tolerance to 36000, retaining the original libjxl conformance PNG
Comment 22•1 day ago
|
||
Comment 23•18 hours ago
|
||
| bugherder | ||
Created web-platform-tests PR https://github.com/web-platform-tests/wpt/pull/63538 for changes under testing/web-platform/tests
Description
•