Search code examples
javascriptcanvasweb-worker

Merge png images using webworkers and canvas


I'm trying to take advantage of web workers in order to merge an array of pngs into a single image with transparency, same like photoshop does when you merge multiple layers.

The reason I'm doing this is because I'm saving olny the result of the combination into indexeddb for offline usage, and after that I'm creating a texture with that image in my three.js app.

The scenario is that there are about 400 products and the texture - image on the 3d plane(product) is a mixture of 3 images (product title, price and product image).

I' ve implemented this so far, but image processing is not that good and I was wondering how could I do that with web workers if i pass them the imageData.data

Here is what I have so far but I can't make it work. (this is done without webworkers for now just trying to make the merge to work)

var images = [];
var imageData = [];

images.push( function ( callback ) {

    var mobile = new Image();

    mobile.onload = function () {
        console.log( "mobile loaded" );
        callback( null, mobile );
    };

    mobile.onerror = function ( e ) {
        console.log( "mobile error" );
        callback( e, null );
    };

    mobile.src = "mobile.png";

} );

images.push( function ( callback ) {

    var text = new Image();

    text.onload = function () {
        console.log( "text loaded" );
        callback( null, text );
    };

    text.onerror = function ( e ) {
        console.log( "text error" );
        callback( e, null );
    };

    text.src = "text.png";

} );

async.series( images, function ( err, results ) {

    if ( err ) {
        console.error( err );
    } else {
        console.log( results );
        getImageData( results );
    }

} );


var getImageData = function ( images ) {


    for ( var i = 0; i < images.length; i++ ) {

        var canvas = document.createElement( 'canvas' );

        canvas.width = 512;
        canvas.height = 512;

        var ctx = canvas.getContext( '2d' );

        ctx.drawImage( images[ i ], 0, 0 );

        imageData.push( ctx.getImageData( 0, 0, canvas.width, canvas.height ).data );

    }

    merge( imageData );

};

var merge = function ( data ) {

    var merged = [];
    var mixFactor = 0.5;

    //var newPixel = imageMainPixel * mixFactor + imageSecPixel * ( 1 - mixFactor )

    //for ( var i = 0, len = data.length; i < len; i++ ) {

    for ( var j = 0, byteLen = data[ 0 ].length; j < byteLen; j += 4 ) {

        var r = data[ 0 ][ j ] * mixFactor + data[ 1 ][ j ] * ( 1 - mixFactor );
        var g = data[ 0 ][ j + 1 ] * mixFactor + data[ 1 ][ j + 1 ] * ( 1 - mixFactor );
        var b = data[ 0 ][ j + 2 ] * mixFactor + data[ 1 ][ j + 2 ] * ( 1 - mixFactor );
        var a = data[ 0 ][ j + 3 ] * mixFactor + data[ 1 ][ j + 3 ] * ( 1 - mixFactor );

        merged[ j ] = r;
        merged[ j + 1 ] = g;
        merged[ j + 2 ] = b;
        merged[ j + 4 ] = a;

    }

    //}

    var canvas = document.getElementById( 'canvas' );
    canvas.width = 512;
    canvas.height = 512;
    var ctx = canvas.getContext( '2d' );
    var imageData = ctx.createImageData( 512, 512 );
    imageData.data.set( merged );
    ctx.putImageData( imageData, 0, 0 );

};

The error I'm currently getting is this:

Uncaught RangeError: Source is too large

UPDATE

I managed to merge the images by changing my merge function into this:

var merge = function ( data ) {

    var canvas = document.getElementById( 'canvas' );
    canvas.width = 512;
    canvas.height = 512;
    var ctx = canvas.getContext( '2d' );
    var imageData = ctx.createImageData( 512, 512 );
    var mixFactor = 0.5;


    for ( var j = 0, byteLen = data[ 0 ].length; j < byteLen; j += 4 ) {

        imageData.data[ j ] = data[ 0 ][ j ] * mixFactor + data[ 1 ][ j ] * ( 1 - mixFactor );
        imageData.data[ j + 1 ] = data[ 0 ][ j + 1 ] * mixFactor + data[ 1 ][ j + 1 ] * ( 1 - mixFactor );
        imageData.data[ j + 2 ] = data[ 0 ][ j + 2 ] * mixFactor + data[ 1 ][ j + 2 ] * ( 1 - mixFactor );
        imageData.data[ j + 3 ] = data[ 0 ][ j + 3 ] * mixFactor + data[ 1 ][ j + 3 ] * ( 1 - mixFactor );

    }


    console.log( imageData.data.length );

    ctx.putImageData( imageData, 0, 0 );

};

The result is like 2 images both with 50% opacity. That is not the desired result though. I would like just the images to act as layers like having multiple pngs one on top of the other... IF the one on top has transparent places you should be able to see the image under it.

Perhaps I should draw based on the alpha value ?


Solution

  • Well after some thinking I ended up checking the alpha value of every pixel, and managed to accomplish what I wanted. I rewrote the merge function like this:

    var merge = function ( data ) {
    
        var canvas = document.getElementById( 'canvas' );
        canvas.width = 512;
        canvas.height = 512;
        var ctx = canvas.getContext( '2d' );
        var imageData = ctx.createImageData( 512, 512 );
        var mixFactor = 0.5;
    
    
        for ( var j = 0, byteLen = data[ 0 ].length; j < byteLen; j += 4 ) {
    
            // imageData.data[ j ] = data[ 0 ][ j ] * mixFactor + data[ 1 ][ j ] * ( 1 - mixFactor );
            // imageData.data[ j + 1 ] = data[ 0 ][ j + 1 ] * mixFactor + data[ 1 ][ j + 1 ] * ( 1 - mixFactor );
            // imageData.data[ j + 2 ] = data[ 0 ][ j + 2 ] * mixFactor + data[ 1 ][ j + 2 ] * ( 1 - mixFactor );
            // imageData.data[ j + 3 ] = data[ 0 ][ j + 3 ] * mixFactor + data[ 1 ][ j + 3 ] * ( 1 - mixFactor );
    
            imageData.data[ j ] = ( data[ 1 ][ j + 3 ] === 0 ? data[ 0 ][ j ] : data[ 1 ][ j ] );
            imageData.data[ j + 1 ] = ( data[ 1 ][ j + 3 ] === 0 ? data[ 0 ][ j + 1 ] : data[ 1 ][ j + 1 ] );
            imageData.data[ j + 2 ] = ( data[ 1 ][ j + 3 ] === 0 ? data[ 0 ][ j + 2 ] : data[ 1 ][ j + 2 ] );
            imageData.data[ j + 3 ] = ( data[ 1 ][ j + 3 ] === 0 ? data[ 0 ][ j + 3 ] : data[ 1 ][ j + 3 ] );
    
        }
    
    
        console.log( imageData.data.length );
    
        ctx.putImageData( imageData, 0, 0 );
    
    };
    

    Some improvements are still needed but I suppose now I'll be able to merge images inside a web worker :)