merging images while maintaing alpha channels

PHP programming forum. Ask questions or help people concerning PHP code. Don't understand a function? Need help implementing a class? Don't understand a class? Here is where to ask. Remember to do your homework!

Moderator: General Moderators

Post Reply
redmonkey
Forum Regular
Posts: 836
Joined: Thu Dec 18, 2003 3:58 pm

merging images while maintaing alpha channels

Post by redmonkey »

I ran into a problem with both imagecopy() and imagecopymerge() in the way they operate when both source and destination images have alpha channels.

Ideally I need to maintain and merge alpha channels while also having the flexibility of imagecopymerge's opacity level parameter. Imaging is not really my area of interest and to be honest my head is starting to hurt trying to work out what should be happening in terms of merging levels.

The issue, and what I have so far......

Using imagecopymerge....

Code: Select all

 
$bg   = imagecreatefrompng('tux.png');
$over = imagecreatefrompng('ff-logo-sm.png');
 
imagealphablending($bg, false);
imagesavealpha($bg, true);
 
imagecopymerge($bg, $over, 276, 300, 0, 0, 123, 119, 100);
imagepng($bg, 'tux-fox-imagecopymerge.png');
 
This results in the alpha channel of the destination image being retained but the alpha channel of the source image is ignored....
Image

Using imagecopy.....

Code: Select all

 
$bg   = imagecreatefrompng('tux.png');
$over = imagecreatefrompng('ff-logo-sm.png');
 
imagealphablending($bg, false);
imagesavealpha($bg, true);
 
imagecopy($bg, $over, 276, 300, 0, 0, 123, 119);
imagepng($bg, 'tux-fox-imagecopy.png');
 

This results in the alpha channels of both images being retained but as the source image takes priority, the level of it's alpha channel is applied without any merging of the underlying/destination image...
Image

So I've come up with 'imagecopymerge_alpha' (callng code, resulting image then actual function code below in that order).....

Code: Select all

 
$bg   = imagecreatefrompng('tux.png');
$over = imagecreatefrompng('ff-logo-sm.png');
 
imagealphablending($bg, false);
imagesavealpha($bg, true);
 
imagecopymerge_alpha($bg, $over, 276, 300, 0, 0, 123, 119, 100);
imagepng($bg, 'tux-fox-imagecopymerge_alpha.png');
 
Which initially has promising results.......
Image
However, there is an issue when I start adding opacity levels other than 100% into the mix, the function code is below followed by some sample images at varying opacity levels. I'm wondering if I've missed a trick somewhere and this could be done with PHP's inbuilt functions? Or if anyone has ran into a similar problem and has already come up with a solution to the opacity problem?

Code: Select all

 
/**
 * merge two true colour images while maintaining alpha transparency of both
 * images.
 *
 * known issues : Opacity values other than 100% get a bit screwy, the source
 *                composition determines how much this issue will annoy you.
 *                if in doubt, use as you would imagecopy_alpha (i.e. keep
 *                opacity at 100%)
 *
 * @access public
 *
 * @param  resource $dst  Destination image link resource
 * @param  resource $src  Source image link resource
 * @param  int      $dstX x-coordinate of destination point
 * @param  int      $dstY y-coordinate of destination point
 * @param  int      $srcX x-coordinate of source point
 * @param  int      $srcY y-coordinate of source point
 * @param  int      $w    Source width
 * @param  int      $h    Source height
 * @param  int      $pct  Opacity or source image
 ******************************************************************************/
function imagecopymerge_alpha($dst, $src, $dstX, $dstY, $srcX, $srcY, $w, $h, $pct)
{
    $pct /= 100;
 
    /* sanity check before going any further */
    $pct  = max(min(1, $pct), 0);
 
    if ($pct == 0)
    {
        /* would anyone really attempt to call us with 0% opacity? */
        return;
    }
 
    $toy  = $dstY;
 
    for ($y = $srcY; $y < ($srcY + $h); $y++)
    {
        $tox = $dstX;
 
        for ($x = $srcX; $x < ($srcX + $w); $x++)
        {
            /* get source image's pixel RGBA index */
            $src_c  = imagecolorat($src, $x, $y);
 
            if ($src_c === false)
            {
                /* we must be 'out of bounds' on source image so skip        */
                /* essentially leaving a fully transparent area through to   */
                /* destination image. is this correct behaviour or should we */
                /* fill black/white ??                                       */
                $tox++;
                continue;
            }
 
            /* get source alpha channel level and decide if we need to continue */
            $src_a  = ($src_c >> 24) & 0xFF;
 
            if ($pct < 1)
            {
                /* fake opacity level by adjusting alpha channel level */
                $src_a = ($src_a == 0) ? 127 - (127 * $pct) : $src_a + (127 * $pct);
                $src_a = ($src_a > 127) ? 127 : (int)$src_a;
            }
 
            if ($src_a == 127)
            {
                /* fully transparent areas of source image can be skipped */
                $tox++;
                continue;
            }
 
            $src_r = ($src_c >> 16) & 0xFF;
            $src_g = ($src_c >>  8) & 0xFF;
            $src_b = ($src_c)       & 0xFF;
 
            /* get destination image's pixel RGBA index */
            $dst_c = imagecolorat($dst, $tox, $toy);
            $dst_a = ($dst_c >> 24) & 0xFF;
            $dst_r = ($dst_c >> 16) & 0xFF;
            $dst_g = ($dst_c >>  8) & 0xFF;
            $dst_b = ($dst_c)       & 0xFF;
 
            /* alpha multiplier */
            $alpha = $src_a / 127;
 
            /* RGB compensation for alpha channel level */
            $new_r = $src_r - ($src_r * $alpha) + ($dst_r * $alpha);
            $new_g = $src_g - ($src_g * $alpha) + ($dst_g * $alpha);
            $new_b = $src_b - ($src_b * $alpha) + ($dst_b * $alpha);
 
            /* sanity check nothing gets above 255 */
            $new_r = ($new_r > 255 )? 255 : (int)$new_r;
            $new_g = ($new_g > 255 )? 255 : (int)$new_g;
            $new_b = ($new_b > 255 )? 255 : (int)$new_b;
 
            /* alpha channel compensation, I doubt it's as simple as this.    */
            /* this requires further investigation, with some of our test     */
            /* images applying even just a little reduction in opacity starts */
            /* to intoduce artifacts around the edges of source's non         */
            /* transparent image area when the  merge area of destination     */
            /* image is fully transparent. it also gets very 'screwy' with    */
            /* small opacity levels. it works for us at the moment but it's   */
            /* not right and I suspect the issue lies here but it may also be */
            /* related to some of the compensation calculations above         */
            $new_a = min($src_a, $dst_a);
 
            if ($new_a > 0)
            {
                $new_a = $dst_a * $alpha;
                $new_a = ($new_a > 127) ? 127 : (int)$new_a;
            }
 
            /* finally get and set this pixel's RGBA colour index */
            $rgba = ImageColorAllocateAlpha($dst, $new_r, $new_g, $new_b, $new_a);
            if ($rgba == -1) {
                $rgba = ImageColorClosestAlpha($dst, $new_r, $new_g, $new_b, $new_a);
            }
 
            imagesetpixel($dst, $tox, $toy, $rgba);
            $tox++;
        }
        $toy++;
    }
}
 
With 80% opacity...
Image

With 40% opacity...
Image

With 20% opacity....
Image

With 10% opacity....
Image
redmonkey
Forum Regular
Posts: 836
Joined: Thu Dec 18, 2003 3:58 pm

Re: merging images while maintaing alpha channels

Post by redmonkey »

Not to worry. After a bit of RTFMing I realised that imagecopy will do the majority of what is required if alphablending is left enabled. The name of the function should've given me some clue but hey ho.
psychotomus
Forum Contributor
Posts: 487
Joined: Fri Jul 11, 2003 1:59 am

Re: merging images while maintaing alpha channels

Post by psychotomus »

Mind if I use your imagecopymerge_alpha function? ;]
redmonkey
Forum Regular
Posts: 836
Joined: Thu Dec 18, 2003 3:58 pm

Re: merging images while maintaing alpha channels

Post by redmonkey »

If you want but after discovering that imagecopy() can handle merging of the alpha channels in the way I was looking the above code isn't very efficient.

Below is a revision which basically adjusts the alpha channel of the source image before throwing it over to imagecopy to do the actual copy work. I also worked out why I was having problems with the edges of the source image when applying opacity so that is also fixed in the function below....

Code: Select all

 
/**
 * merge two true colour images with variable opacity while maintaining alpha
 * transparency of both images.
 *
 * @access public
 *
 * @param  resource $dst  Destination image link resource
 * @param  resource $src  Source image link resource
 * @param  int      $dstX x-coordinate of destination point
 * @param  int      $dstY y-coordinate of destination point
 * @param  int      $srcX x-coordinate of source point
 * @param  int      $srcY y-coordinate of source point
 * @param  int      $w    Source width
 * @param  int      $h    Source height
 * @param  int      $pct  Opacity of source image
 ******************************************************************************/
function imagecopymerge_alpha($dst, $src, $dstX, $dstY, $srcX, $srcY, $w, $h, $pct)
{
    $pct /= 100;
 
    /* make sure opacity level is within range before going any further */
    $pct  = max(min(1, $pct), 0);
 
    if ($pct == 0)
    {
        /* 0% opacity? then we have nothing to do */
        return;
    }
 
    /* work out if we need to bother correcting for opacity */
    if ($pct < 1)
    {
        /* we need a copy of the original to work from, only copy the cropped */
        /* area of src                                                        */
        $srccopy  = imagecreatetruecolor($w, $h);
 
        /* attempt to maintain alpha levels, alpha blending must be *off* */
        imagealphablending($srccopy, false);
        imagesavealpha($srccopy, true);
 
        imagecopy($srccopy, $src, 0, 0, $srcX, $srcY, $w, $h);
 
        /* we need to know the max transaprency of the image */
        $max_t = 0;
 
        for ($y = 0; $y < $h; $y++)
        {
            for ($x = 0; $x < $w; $x++)
            {
                $src_c = imagecolorat($srccopy, $x, $y);
                $src_a = ($src_c >> 24) & 0xFF;
 
                $max_t = $src_a > $max_t ? $src_a : $max_t;
            }
        }
        /* src has no transparency? set it to use full alpha range */
        $max_t = $max_t == 0 ? 127 : $max_t;
 
        /* $max_t is now being reused as the correction factor to apply based */
        /* on the original transparency range of  src                         */
        $max_t /= 127;
 
        /* go back through the image adjusting alpha channel as required */
        for ($y = 0; $y < $h; $y++)
        {
            for ($x = 0; $x < $w; $x++)
            {
                $src_c  = imagecolorat($src, $srcX + $x, $srcY + $y);
                $src_a  = ($src_c >> 24) & 0xFF;
                $src_r  = ($src_c >> 16) & 0xFF;
                $src_g  = ($src_c >>  8) & 0xFF;
                $src_b  = ($src_c)       & 0xFF;
 
                /* alpha channel compensation */
                $src_a = ($src_a + 127 - (127 * $pct)) * $max_t;
                $src_a = ($src_a > 127) ? 127 : (int)$src_a;
 
                /* get and set this pixel's adjusted RGBA colour index */
                $rgba  = ImageColorAllocateAlpha($srccopy, $src_r, $src_g, $src_b, $src_a);
 
                /* ImageColorAllocateAlpha returns -1 for PHP versions prior  */
                /* to 5.1.3 when allocation failed                               */
                if ($rgba === false || $rgba == -1)
                {
                    $rgba = ImageColorClosestAlpha($srccopy, $src_r, $src_g, $src_b, $src_a);
                }
 
                imagesetpixel($srccopy, $x, $y, $rgba);
            }
        }
 
        /* call imagecopy passing our alpha adjusted image as src */
        imagecopy($dst, $srccopy, $dstX, $dstY, 0, 0, $w, $h);
 
        /* cleanup, free memory */
        imagedestroy($srccopy);
        return;
    }
 
    /* still here? no opacity adjustment required so pass straight through to */
    /* imagecopy rather than imagecopymerge to retain alpha channels          */
    imagecopy($dst, $src, $dstX, $dstY, $srcX, $srcY, $w, $h);
    return;
}
 
And it is called like so....

Code: Select all

 
$bg   = imagecreatefrompng('tux.png');
$over = imagecreatefrompng('ff-logo-sm.png');
imagealphablending($bg, true); /* <- must be set to retain alpha blending/merging */
imagesavealpha($bg, true);
imagecopymerge_alpha($bg, $over, 276, 300, 0, 0, 123, 119, 100);
imagepng($bg, 'tux-fox-imagecopymerge.png');
 
Which results like this.....

With 80% opacity...
Image

With 40% opacity...
Image

With 20% opacity...
Image
Post Reply