EDITED, see end of question.
In my application I have two canvas elements. One shows layered, transparent pngs, the other one gets an image from a file input and masks it. The chosen image is transparent where it is not masked. This image is then converted to a dataUrl, transformed to fit into the first canvas and added as the top layer of the first canvas.
Everything works as expected on desktop browsers: Chrome OSX, Safari OSX. I only add it in on load, so I made sure no race conditions can occur.
On Android Chrome and Safari iOS the canvas converted todataURL is rendered transparent. If I add a non-transparent image to the second canvas, the rendered image will show even on mobile devices.
To check I added the supposedly transparent canvas to the body. It shows correctly on desktop, but is transparent on mobile Browsers. Here the simplified JS. I am using fabric.js for convenience, but the problem is the same without the lib. I even once added a background color. Then only the color will show. Any ideas why todataurl on mobile browsers renders only transparent pixels?
<body>
<canvas id="canv"></canvas>
<script src="fabric.js"></script>
<script>
// main canvas
var c = new fabric.Canvas('canv');
c.setWidth(200);
c.setHeight(200);
var i = document.createElement('img');
i.src = 'dummy.jpg';
// i.src = 'dummy1.png';
i.onload = function(e) {
//document.body.appendChild(i);
scale = 1; // resizes the image
var ci = new fabric.Image(i);
ci.set({
left: 0,
top: 0,
scaleX: scale,
scaleY: scale,
originX: 'left',
originY: 'top'
}).setCoords();
// temporary canvas, will be converted to dataurl, contains transformed image
var tmpCanvas = new fabric.Canvas();
tmpCanvas.setWidth(100);
tmpCanvas.setHeight(100);
ci.scaleToWidth(100);
tmpCanvas.add(ci);
tmpCanvas.renderAll();
// create image from temporary canvas
var customImage = new fabric.Image.fromURL(tmpCanvas.toDataURL({ format: 'png' }), function (cImg) {
// add it to original canvas
c.clear();
c.add(cImg);
c.renderAll();
data = c.toDataURL({ format: 'png' });
// resized image
var newc = new fabric.StaticCanvas().setWidth(300).setHeight(300);
var newImg = new fabric.Image.fromURL(data, function (c1Img) {
newc.add(c1Img);
newc.renderAll();
// append to body to check if canvas is rendered correctly
document.body.appendChild(newc.lowerCanvasEl);
});
});
}
</script>
EDIT: I solved the problem, but could not find the problem on the Javascript side.
The problem was that I copied a temporary canvas onto another canvas. The scale and position of the added canvas was computed by finding the bounding box of non transparent pixels in a png, which was generated exactly for this purpose. A mask in short.
The bounding box was calculated in another temporary canvas at the start of the app (based on this answer). Although all sizes of the mask and its canvas were set correctly and the canvas was never added to the DOM, when loaded on a small screen the results of the bounding box differed from from the full screen results. After much testing i found this was true on Desktop too.
Because I already spent so much time on the problem, I decided to try to calculate the bounds in PHP and put it into a data attribute. Which worked great!
For those interested in the PHP solution:
function get_bounding_box($imgPath) {
$img = imagecreatefrompng($imgPath);
$w = imagesx($img);
$h = imagesy($img);
$bounds = [
'left' => $w,
'right' => 0,
'top' => $h,
'bottom' => 0
];
// get alpha of every pixel, if it is not fully transparent, write it to bounds
for ($yPos = 0; $yPos < $h; $yPos++) {
for ($xPos = 0; $xPos < $w; $xPos++) {
// Check, ob Pixel nicht vollständig transparent ist
$rgb = imagecolorat($img, $xPos, $yPos);
if (imagecolorsforindex($img, $rgb)['alpha'] < 127) {
if ($xPos < $bounds['left']) {
$bounds['left'] = $xPos;
}
if ($xPos > $bounds['right']) {
$bounds['right'] = $xPos;
}
if ($yPos < $bounds['top']) {
$bounds['top'] = $yPos;
}
if ($yPos > $bounds['bottom']) {
$bounds['bottom'] = $yPos;
}
}
}
}
return $bounds;
}
The problem was that I copied a temporary canvas onto another canvas. The scale and position of the added canvas was computed by finding the bounding box of non transparent pixels in a png, which was generated exactly for this purpose. A mask in short.
The bounding box was calculated in another temporary canvas at the start of the app (based on this answer). Although all sizes of the mask and its canvas were set correctly and the canvas was never added to the DOM, when loaded on a small screen the results of the bounding box differed from from the full screen results. After much testing i found this was true on Desktop too.
Because I already spent so much time on the problem, I decided to try to calculate the bounds in PHP and put it into a data attribute. Which worked great!
For those interested in the PHP solution:
function get_bounding_box($imgPath) {
$img = imagecreatefrompng($imgPath);
$w = imagesx($img);
$h = imagesy($img);
$bounds = [
'left' => $w,
'right' => 0,
'top' => $h,
'bottom' => 0
];
// get alpha of every pixel, if it is not fully transparent, write it to bounds
for ($yPos = 0; $yPos < $h; $yPos++) {
for ($xPos = 0; $xPos < $w; $xPos++) {
// Check, ob Pixel nicht vollständig transparent ist
$rgb = imagecolorat($img, $xPos, $yPos);
if (imagecolorsforindex($img, $rgb)['alpha'] < 127) {
if ($xPos < $bounds['left']) {
$bounds['left'] = $xPos;
}
if ($xPos > $bounds['right']) {
$bounds['right'] = $xPos;
}
if ($yPos < $bounds['top']) {
$bounds['top'] = $yPos;
}
if ($yPos > $bounds['bottom']) {
$bounds['bottom'] = $yPos;
}
}
}
}
return $bounds;
}