简体   繁体   中英

Change html canvas black background to white background when creating jpg image from png image

I have a canvas which is loaded with a png image. I get its jpg base64 string by .toDataURL() method like this:

 $('#base64str').val(canvas.toDataURL("image/jpeg"));

But the transparent parts of the png image are shown black in the new jpg image.

Any solutions to change this color to white? Thanks in advance.

This blackening occurs because the 'image/jpeg' conversion involves setting the alpha of all canvas pixels to fully opaque (alpha=255). The problem is that transparent canvas pixels are colored fully-black-but-transparent . So when you turn these black pixels opaque, the result is a blackened jpeg.

The workaround is to manually change all non-opaque canvas pixels to your desired white color instead of black.

That way when they are made opaque they will appear as white instead of black pixels.

Here's how:

// change non-opaque pixels to white
var imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
var data=imgData.data;
for(var i=0;i<data.length;i+=4){
    if(data[i+3]<255){
        data[i]=255;
        data[i+1]=255;
        data[i+2]=255;
        data[i+3]=255;
    }
}
ctx.putImageData(imgData,0,0);

This answer is a bit longer, but I find it to be more 'correct' in that it handles these things without directly modifying raw canvas data. I find that to be a pretty messy and theoretically unsatisfying solution. There are built in functions to achieve that, and they ought to be used. Here is the solution I found/pilfered:

function canvasToImage(backgroundColor){

var context = document.getElementById('canvas').getContext('2d');

canvas = context.canvas;
//cache height and width        
var w = canvas.width;
var h = canvas.height;

var data;

//get the current ImageData for the canvas.
data = context.getImageData(0, 0, w, h);

//store the current globalCompositeOperation
var compositeOperation = context.globalCompositeOperation;

//set to draw behind current content
context.globalCompositeOperation = "destination-over";

//set background color
context.fillStyle = backgroundColor;

//draw background / rect on entire canvas
context.fillRect(0,0,w,h);

//get the image data from the canvas
var imageData = this.canvas.toDataURL("image/jpeg");

//clear the canvas
context.clearRect (0,0,w,h);

//restore it with original / cached ImageData
context.putImageData(data, 0,0);

//reset the globalCompositeOperation to what it was
context.globalCompositeOperation = compositeOperation;

//return the Base64 encoded data url string
return imageData;
}

Basically, you create a white background image and underlay it under the canvas and then print that. This function is mostly plagiarized from someone's blog, but it required a bit of modification -- such as actually getting the context -- and copied directly from my (working) code, so as long as your canvas element has the id 'canvas', you should be able to copy/paste it and have it work.

This is the blog post I modified it from:

http://www.mikechambers.com/blog/2011/01/31/setting-the-background-color-when-generating-images-from-canvas-todataurl/

The big advantage of my function over this is that it outputs to jpeg instead of png, which is more likely to work well in chrome, which has a dataurl limit of 2MB, and it actually grabs the context, which was a glaring omission in the original function.

After spending a lot of time on this and this post specifically, and these solutions kinda worked expect I just couldn't get the canvas to look right. Anyway I found this solution elsewhere and wanted to post it here incase it helps someone else from spending hours trying to get the black background to white and look like the original.

public getURI(): string {
    let canvas = <HTMLCanvasElement>document.getElementById('chartcanvas');
    var newCanvas = <HTMLCanvasElement>canvas.cloneNode(true);
    var ctx = newCanvas.getContext('2d');
    ctx.fillStyle = "#FFF";
    ctx.fillRect(0, 0, newCanvas.width, newCanvas.height);
    ctx.drawImage(canvas, 0, 0);
    return newCanvas.toDataURL("image/jpeg");
}

Marks answer is correct, but when a picture has some antialiasing applied, the exported image won't be as good as it should be (mainly text). I would like to enhance his solution:

// change non-opaque pixels to white
var imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
var data=imgData.data;
for(var i=0;i<data.length;i+=4){
    if(data[i+3]<255){
        data[i] = 255 - data[i];
        data[i+1] = 255 - data[i+1];
        data[i+2] = 255 - data[i+2];
        data[i+3] = 255 - data[i+3];
    }
}
ctx.putImageData(imgData,0,0);

如果您只想将全透明像素移至白色,只需检查 (data[i+3]==0) 而不是 (data[i+3]<255)。

// change non-opaque pixels to white
var imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
var data=imgData.data;
for(var i=0;i<data.length;i+=4){
    if(data[i+3]<255){
        data[i] = 255 - data[i];
        data[i+1] = 255 - data[i+1];
        data[i+2] = 255 - data[i+2];
        data[i+3] = 255 - data[i+3];
    }

Here is my function that resizes a photo and handles the black transparent background problem:

resizeImage({ file, maxSize, backgroundColor }) {
    const fr = new FileReader();
    const img = new Image();

    const dataURItoBlob = (dataURI) => {
        const bytes = (dataURI.split(',')[0].indexOf('base64') >= 0)
            ? window.atob(dataURI.split(',')[1])
            : window.unescape(dataURI.split(',')[1]);
        const mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
        const max = bytes.length;
        const ia = new Uint8Array(max);
        for (let i = 0; i < max; i += 1) {
            ia[i] = bytes.charCodeAt(i);
        }
        return new Blob([ia], { type: mime });
    };

    const resize = () => {
        // create a canvas element to manipulate
        const canvas = document.createElement('canvas');
        canvas.setAttribute('id', 'canvas');
        const context = canvas.getContext('2d');

        // setup some resizing definitions
        let { width, height } = img;
        const isTooWide = ((width > height) && (width > maxSize));
        const isTooTall = (height > maxSize);

        // resize according to `maxSize`
        if (isTooWide) {
            height *= maxSize / width;
            width = maxSize;
        } else if (isTooTall) {
            width *= maxSize / height;
            height = maxSize;
        }

        // resize the canvas
        canvas.width = width;
        canvas.height = height;

        // place the image on the canvas
        context.drawImage(img, 0, 0, width, height);

        // get the current ImageData for the canvas
        const data = context.getImageData(0, 0, width, height);

        // store the current globalCompositeOperation
        const compositeOperation = context.globalCompositeOperation;

        // set to draw behind current content
        context.globalCompositeOperation = 'destination-over';

        // set background color
        context.fillStyle = backgroundColor;

        // draw background / rect on entire canvas
        context.fillRect(0, 0, width, height);

        // get the image data from the canvas
        const imageData = canvas.toDataURL('image/jpeg');

        // clear the canvas
        context.clearRect(0, 0, width, height);

        // restore it with original / cached ImageData
        context.putImageData(data, 0, 0);

        // reset the globalCompositeOperation to what it was
        context.globalCompositeOperation = compositeOperation;

        // return the base64-encoded data url string
        return dataURItoBlob(imageData);
    };

    return new Promise((resolve, reject) => {
        if (!file.type.match(/image.*/)) {
            reject(new Error('VImageInput# Problem resizing image: file must be an image.'));
        }

        fr.onload = (readerEvent) => {
            img.onload = () => resolve(resize());
            img.src = readerEvent.target.result;
        };

        fr.readAsDataURL(file);
    });
},

That is a Vue JS instance method that can be used like this:

// this would be the user-uploaded file from the input element
const image = file;

const settings = {
    file: image,
    maxSize: 192, // to make 192x192 image
    backgroundColor: '#FFF',
};

// this will output a base64 string you can dump into your database
const resizedImage = await this.resizeImage(settings);

My solution here is a combination of about 74 different StackOverflow answers related to resizing images client-side, and the final boss was to handle transparent PNG files.

My answer would not be possible without Laereom's answer here.

为什么不将其保存为 PNG?

canvas.toDataURL("image/png");

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM