PHP wrong result for imagetruecolortopalette with PNG with transparency PHP wrong result for imagetruecolortopalette with PNG with transparency php php

PHP wrong result for imagetruecolortopalette with PNG with transparency


You can do that quite easily in ImageMagick, which is distributed on Linux and is available for Windows and Mac OSX. There are also many APIs other than the command line. Here is how to do it in ImageMagick command line.

Input:

enter image description here

convert image.png PNG8:result1.png


enter image description here

PNG8: means 256 colors and binary transparency. That means either full or no transparency. This causes the aliasing (stair-stepping) around the edges. If you are willing to set a background color in place of transparency, then you can keep the smooth (antialiased) outline in the result. So for a white background.

convert image.png -background white -flatten PNG8:result2.png


enter image description here

ImageMagick is run by PHP Imagick. So you should be able to do that with PHP Imagick. Or you can call ImageMagick command line from PHP exec().


Updated Answer

I had a a bit more time to work out the full code to answer you - I have simplified what you had quite considerably and it seems to do what I think you want now!

#!/usr/bin/php -f<?phpfunction extractAlpha($im){   // Ensure input image is truecolour, not palette   if(!imageistruecolor($im)){      printf("DEBUG: Converting input image to truecolour\n");      imagepalettetotruecolor($im);   }   // Get width and height   $w = imagesx($im);   $h = imagesy($im);   // Allocate a new greyscale, palette (non-alpha!) image to hold the alpha layer, since it only needs to hold alpha values 0..127   $alpha = imagecreate($w,$h);   // Create a palette for 0..127   for($i=0;$i<128;$i++){      imagecolorallocate($alpha,$i,$i,$i);   }   for ($x = 0; $x < $w; $x++) {      for ($y = 0; $y < $h; $y++) {         // Get current color         $rgba = imagecolorat($im, $x, $y);         // $r = ($rgba >> 16) & 0xff;         // $g = ($rgba >> 8) & 0xff;         // $b = $rgba & 0xf;         $a = ($rgba & 0x7F000000) >> 24;         imagesetpixel($alpha,$x,$y,$a);         //printf("DEBUG: alpha[%d,%d] = %d\n",$x,$y,$a);      }   }   return $alpha;}function applyAlpha($im,$alpha){   // If output image is truecolour   //    iterate over pixels getting current color and just replacing alpha component   // else (palettised)   //    // find a transparent colour in the palette   //    if not successful   //       allocate transparent colour in palette   //    iterate over pixels replacing transparent ones with allocated transparent colour   // Get width and height   $w = imagesx($im);   $h = imagesy($im);   // Ensure all the lovely new alpha we create will be saved when written to PNG    imagealphablending($im, false);   imagesavealpha($im, true);   // If output image is truecolour, we can set alpha 0..127   if(imageistruecolor($im)){      printf("DEBUG: Target image is truecolour\n");      for ($x = 0; $x < $w; $x++) {         for ($y = 0; $y < $h; $y++) {            // Get current color             $rgba = imagecolorat($im, $x, $y);            // Get alpha            $a = imagecolorat($alpha,$x,$y);            // printf("DEBUG: Setting alpha[%d,%d] = %d\n",$x,$y,$a);            $new = ($rgba & 0xffffff) | ($a<<24);            imagesetpixel($im,$x,$y,$new);         }      }   } else {      printf("DEBUG: Target image is palettised\n");      // Must be palette image, get index of a fully transparent color      $transp = -1;      for($index=0;$index<imagecolorstotal($im);$index++){         $c = imagecolorsforindex($im,$index);         if($c["alpha"]==127){            $transp = $index;            printf("DEBUG: Found a transparent colour at index %d\n",$index);         }      }      // If we didn't find a transparent colour in the palette, allocate one      $transp = imagecolorallocatealpha($im,0,0,0,127);      // Scan image replacing all pixels that are transparent in the original copied alpha channel with the index of a transparent pixel in current palette      for ($x = 0; $x < $w; $x++) {         for ($y = 0; $y < $h; $y++) {            // Essentially we are thresholding the alpha here. If it was more than 50% transparent in original it will become fully trasnparent now            $grey = imagecolorat($alpha,$x,$y) & 0xFF;            if($grey>64){               //printf("DEBUG: Replacing transparency at %d,%d\n",$x,$y);               imagesetpixel($im,$x,$y,$transp);            }         }      }   }   return $im;}// Set new width and height$wNew = 300;$hNew = 400;// Open input image and get dimensions$src = imagecreatefrompng('tux.png');$w = imagesx($src);$h = imagesy($src);// Extract the alpha and save as greyscale for inspection$alpha = extractAlpha($src);// Resize alpha to match resized source image$alpha = imagescale($alpha,$wNew,$hNew,IMG_NEAREST_NEIGHBOUR);imagepng($alpha,'alpha.png');// Resize original image$resizedImg = imagecreatetruecolor($wNew, $hNew);imagecopyresampled($resizedImg, $src, 0, 0, 0, 0, $wNew, $hNew, $w, $h);// Palettiseimagetruecolortopalette($resizedImg, true, 250);// Apply extracted alpha and save$res = applyAlpha($resizedImg,$alpha);imagepng($res,'result.png');?>

Result

enter image description here

Extracted alpha channel:

enter image description here

Original Answer

I created a PHP function to extract the alpha channel from an image, and then to apply that alpha channel to another image.

If you apply the copied alpha channel to a truecolour image, it will permit a smooth alpha with 7-bit resolution, i.e. up to 127. If you apply the copied alpha to a palettised image, it will threshold it at 50% (you can change it) so that the output image has binary (on/off) alpha.

So, I extracted the alpha from this image - you can hopefully see there is an alpha ramp/gradient in the middle.

enter image description here

And applied the copied alpha to this image.

enter image description here

Where the second image was truecolour, the alpha comes across like this:

enter image description here

Where the second image was palettised, the alpha comes across like this:

enter image description here

The code should be pretty self-explanatory. Uncomment printf() statements containing DEBUG: for lots of output:

#!/usr/bin/php -f<?php// Make test images with ImageMagick as follows:// convert -size 200x100 xc:magenta  \( -size 80x180 gradient: -rotate 90 -bordercolor white  -border 10 \) -compose copyopacity -composite png32:image1.png// convert -size 200x100 xc:blue image2.png       # Makes palettised image// or// convert -size 200x100 xc:blue PNG24:image2.png # Makes truecolour imagefunction extractAlpha($im){   // Ensure input image is truecolour, not palette   if(!imageistruecolor($im)){      printf("DEBUG: Converting input image to truecolour\n");      imagepalettetotruecolor($im);   }   // Get width and height   $w = imagesx($im);   $h = imagesy($im);   // Allocate a new greyscale, palette (non-alpha!) image to hold the alpha layer, since it only needs to hold alpha values 0..127   $alpha = imagecreate($w,$h);   // Create a palette for 0..127   for($i=0;$i<128;$i++){      imagecolorallocate($alpha,$i,$i,$i);   }   for ($x = 0; $x < $w; $x++) {      for ($y = 0; $y < $h; $y++) {         // Get current color         $rgba = imagecolorat($im, $x, $y);         // $r = ($rgba >> 16) & 0xff;         // $g = ($rgba >> 8) & 0xff;         // $b = $rgba & 0xf;         $a = ($rgba & 0x7F000000) >> 24;         imagesetpixel($alpha,$x,$y,$a);         //printf("DEBUG: alpha[%d,%d] = %d\n",$x,$y,$a);      }   }   return $alpha;}function applyAlpha($im,$alpha){   // If image is truecolour   //    iterate over pixels getting current color and just replacing alpha component   // else (palettised)   //    allocate a transparent black in the palette   //    if not successful   //       find any other transparent colour in palette   //    iterate over pixels replacing transparent ones with allocated transparent colour   // We expect the alpha image to be non-truecolour, i.e. palette-based - check!   if(imageistruecolor($alpha)){      printf("ERROR: Alpha image is truecolour, not palette-based as expected\n");   }   // Get width and height   $w = imagesx($im);   $h = imagesy($im);   // Ensure all the lovely new alpha we create will be saved when written to PNG    imagealphablending($im, false);   imagesavealpha($im, true);   if(imageistruecolor($im)){      printf("DEBUG: Target image is truecolour\n");      for ($x = 0; $x < $w; $x++) {         for ($y = 0; $y < $h; $y++) {            // Get current color             $rgba = imagecolorat($im, $x, $y);            // Get alpha            $a = imagecolorat($alpha,$x,$y);            // printf("DEBUG: Setting alpha[%d,%d] = %d\n",$x,$y,$a);            $new = ($rgba & 0xffffff) | ($a<<24);            imagesetpixel($im,$x,$y,$new);         }      }   } else {      printf("DEBUG: Target image is palettised\n");      // Must be palette image, get index of a fully transparent color      $trans = imagecolorallocatealpha($im,0,0,0,127);      if($trans===FALSE){         printf("ERROR: Failed to allocate a transparent colour in palette. Either pass image with fewer colours, or look through palette and re-use some other index with alpha=127\n");      } else {         // Scan image replacing all pixels that are transparent in the original copied alpha channel with the index of a transparent pixel in current palette         for ($x = 0; $x < $w; $x++) {            for ($y = 0; $y < $h; $y++) {               // Essentially we are thresholding the alpha here. If it was more than 50% transparent in original it will become fully trasnparent now               if (imagecolorat($alpha,$x,$y) > 64){                  imagesetpixel($im,$x,$y,$trans);                  //printf("DEBUG: Setting alpha[%d,%d]=%d\n",$x,$y,$trans);               }            }         }      }   }   return $im;}// Open images to copy alpha from and to$src = imagecreatefrompng('image1.png');$dst = imagecreatefrompng('image2.png');// Extract the alpha and save as greyscale for inspection$alpha = extractAlpha($src);imagepng($alpha,'alpha.png');// Apply extracted alpha to second image and save$res = applyAlpha($dst,$alpha);imagepng($res,'result.png');?>

Here is the extracted alpha layer, just for fun. Note it is actually a greyscale image representing the alpha channel - it does not have any alpha component itself.

enter image description here

Keywords: PHP, gd, image, image processing, alpha, alpha layer, extract alpha, copy alpha, apply alpha, replace alpha.


I don't think this is strange behavior.

The PHP documentation doesn't say this, but I guess that imagefill() works as in most other applications: by filling connected pixels with the same color as the pixel where the fill started (0, 0).

Because you first set the pallet to 255 pixels (or 256) you convert all dark areas to a black color and loose all transparency.When you then flood fill starting at the left top all connected pixels (also inside the penguin and duck) will become transparent.

I think the only way to do this without ImageMagick is to traverse all pixels of the resized image and to manually set the pixel color to a limited pallet.

Some time ago I wrote a small script that reduces the colors of a PNG while keeping the complete alpha info (1).This will reduce the pallet the PNG file uses and thus the file size. It doesn't matter much if the resulting PNG is still more than 8 bits. A small pallet will reduce the file size anyway.

(1) https://bitbucket.org/thuijzer/pngreduce/

Edit: I just used your resized PNG (with transparency) as input for my script and converted it from a 12 kB to a 7 kB file using only 32 colors:

Reduced to 62.28% of original, 12.1kB to 7.54kB

PNG reduced to 32 colors

Edit 2: I updated my script and added optional Floyd–Steinberg dithering.A result with 16 colors per channel:

Reduced to 66.94% of original, 12.1kB to 8.1kB

enter image description here

Note that dithering also effects the file size because it is 'harder' to compress a PNG when neighboring pixels have different colors.