19

I'm trying to write a PHP script that resizes a PNG image and then converts it to PNG-8 bit mode. So the size of the resulting file will be smaller but without too much quality loss.

The resize works perfectly, preserving also image transparency:

originalImage

The problem is when I convert the image in 8 bit:

imagetruecolortopalette($resizedImg, true, 255);

imagealphablending($resizedImg, false);

$transparent = imagecolorallocatealpha($resizedImg, 255, 255, 255, 127);
if(!imagefill($resizedImg, 0, 0, $transparent)) return false;

imagesavealpha($resizedImg, true);

The resulting image is this, with the transparency all around and a little inside the image:

enter image description here

If I set 256 colors instead of 255:

imagetruecolortopalette($resizedImg, true, 256);

the image will be with black background:

enter image description here

A similar result occurs with this image (note the half transparency for the case with 255 colors):

Original: enter image description here 255 colors: enter image description here 256 colors: enter image description here

The complete function's code:

function resizePng($originalPath, $xImgNew='', $yImgNew='', $newPath='')
{
    if(!trim($originalPath) || !$xyOriginalPath = getimagesize("$originalPath")) return false;
    list($xImg, $yImg) = $xyOriginalPath;
    
    if(!$originalImg = imagecreatefrompng($originalPath)) return false;
    
    if(!$resizedImg = imagecreatetruecolor($xImgNew, $yImgNew)) return false;
    
    // preserve alpha
    imagealphablending($resizedImg, false);
    $transparent = imagecolorallocatealpha($resizedImg, 255, 255, 255, 127);
    if(!imagefill($resizedImg, 0, 0, $transparent)) return false;
    imagesavealpha($resizedImg, true);
    
    // copy content from originalImg to resizedImg
    if(!imagecopyresampled($resizedImg, $originalImg, 0, 0, 0, 0, $xImgNew, $yImgNew, $xImg, $yImg)) return false;
    
    // PNG-8 bit conversion
    imagetruecolortopalette($resizedImg, true, 255);
    
    // preserve alpha
    imagealphablending($resizedImg, false);
    $transparent = imagecolorallocatealpha($resizedImg, 255, 255, 255, 127);
    if(!imagefill($resizedImg, 0, 0, $transparent)) return false;
    imagesavealpha($resizedImg, true);
    
    if(!imagepng($resizedImg, ($newPath) ?: null, 8)) return false;

    return true;
}

What I tried:

https://stackoverflow.com/a/8144620/2342558

// PNG-8 bit conversion
imagetruecolortopalette($resizedImg, true, 255);

imagesavealpha($resizedImg, true);
imagecolortransparent($resizedImg, imagecolorat($resizedImg,0,0));

// preserve alpha
imagealphablending($resizedImg, false);
$transparent = imagecolorallocatealpha($resizedImg, 255, 255, 255, 127);
if(!imagefill($resizedImg, 0, 0, $transparent)) return false;
imagesavealpha($resizedImg, true);

if(!imagepng($resizedImg, ($newPath) ?: null, 8)) return false;

Results:

enter image description here enter image description here

Also: https://stackoverflow.com/a/55402802/2342558

Nothing changed.

Also: others SO posts and some on the Web

Also without resizing the image (removing imagecopyresampled and adapting the variables name) the result is the same.

How can I make it work and to understand the reason for this strange behaviour?

Some info in phpinfo():

  • PHP 7.0.33
  • GD bundled (2.1.0 compatible)
  • PNG Support enabled
  • libPNG 1.5.13.

Edit:

In GIMP v.2.8.22 I can save an image for Web with these properties:

PNG-8
256 colors palette
Dither: Floyd-Steinberg / Floyd-Steinberg 2 / positioned

enter image description here enter image description here

and it produce a reduced image almost identical of the original.

Also pngquant, tinypng, and many others do the same work, but I need to do it with PHP.

Edit 2:

Unfortunately, I can't use ImageMagick because my code is in a shared hosting without it installed.

Edit 3:

in phpinfo() results that the imagemagick module isn't installed.

Edit 4:

Let me do some tests with your responses, maybe there is a solution with only PHP.

Edit 5:

These are my attempts with your answers.

Note: I put an underlying grid to better show the alpha.

Thomas Huijzer's answer:

enter image description here enter image description here

There are visible color banding in the penguin but the duck its ok (although sometimes the color tone is darker).

EPB's answer:

enter image description here enter image description here

Only if the image has only pixels already completely transparent does it work very well (e.g. the duck).

Mark Setchell's answer:

enter image description here enter image description here

It makes completely transparent all pixels with an alpha, also if this alpha is very low, see the shadow below the penguin. Also some pixel at the edge of the duck are converted in black pixel or in full-transparent pixel.

halfer
  • 19,824
  • 17
  • 99
  • 186
user2342558
  • 5,567
  • 5
  • 33
  • 54
  • 1
    There are *significantly* more than 256 colours in that image, not to mention the anti-aliased edge having partially-transparent pixels, greatly adding to the number of palette entries needed. PNG-8 will *not* be able to contain this image. – Niet the Dark Absol Sep 20 '19 at 11:07
  • @NiettheDarkAbsol why pngquant and GIMP can convert them to 8 bit? – user2342558 Sep 20 '19 at 11:14
  • Because they use algorithms to figure out which colours to keep and which are close enough to another colour that they can be discarded. Zoom in close and convert to Indexed in GIMP and you'll see clear banding. – Niet the Dark Absol Sep 20 '19 at 11:16
  • Sure, 8 bit saved with GIMP, when zoomed in show clear banding but often these banding are too little to note. And I don't need to zoom in the image, I need to show it in an Android App without the zoom feature. – user2342558 Sep 20 '19 at 11:19
  • The point is GIMP has algorithms to do it. PHP does not have that natively, you'd have to implement it yourself. Since you're losing a significant amount of image quality by doing this, are you sure it's needed? What are you trying to achieve? – Niet the Dark Absol Sep 20 '19 at 11:25
  • I'm making an Android App which shows full-screen images with ViewPager, so the file size of these images must be more less possible, without too much compromise the quality. The images are edited with PHP. They are more than 1000 images, so I can't reduce them maually with GIMP or pngquant... – user2342558 Sep 20 '19 at 11:31
  • Instead of taking such a complicated approach, why not have different sizes of the image and select them based on the viewport via media query? – EternalHour Sep 20 '19 at 16:10
  • I already have different sizes for each image. I need to reduce their file size. – user2342558 Sep 20 '19 at 16:31
  • 1
    If gimp does the job as you wish, use its [batch mode](https://www.gimp.org/tutorials/Basic_Batch/) to convert your 1000+ files and you're done. – Zeitounator Sep 21 '19 at 13:19
  • I need to do the work with PHP... – user2342558 Sep 21 '19 at 15:54
  • Does the image need to be 8bit? Why not save the image as 24bit? – Martin Sep 22 '19 at 14:42
  • In 8 bit the file size is smaller for about 70 % and resources decryption is much faster. And then in the App I'll put many more images – user2342558 Sep 22 '19 at 15:25
  • Seems like GD does not have the complexity you want in the dither, and that would be the reason of your issue. Maybe this could be a solution: https://stackoverflow.com/questions/53589407/converting-rgb-image-to-floyd-steinberg-image-using-php-or-javascript-for-zebra – β.εηοιτ.βε Sep 23 '19 at 21:15
  • 1
    Two things... firstly, you can *"shell out"* to GIMP from PHP with `system()`. Secondly, your half transparent 255 duck is caused by flood-filling from the top-left but the duck stops it flowing around because it touches the edge and the flood only fills white pixels matching top-left corner not duck-coloured pixels. The solution is to add a 1-pixel wide border all around the image the same colour as top-left pixel so flood-fill can flow all the way around. – Mark Setchell Sep 24 '19 at 20:52
  • It seems you are using a *"Save for Web"* plugin which is no longer available for GIMP v2.10 - is that correct? Are you able to make PNG that is acceptable to you with that plugin and Email it to me please so I can try and assist further? mark@thesetchells.com – Mark Setchell Sep 25 '19 at 06:43
  • @MarkSetchell because of I'm running PHP on a web shared hosting I haven't GIMP installed on the server. Thanks for explained the flood-filling: it answers one of my questions :) About the "Save for Web" plugin, I'm using GIMP v2.8.22 on Ubuntu 18.04. Can you download the latest version? It may have this plugin. – user2342558 Sep 25 '19 at 07:33
  • It's ok if you run GIMP on Ubuntu and send me the image - I just want to test a theory about PNG files to be able to help you. – Mark Setchell Sep 25 '19 at 07:34
  • @MarkSetchell if I correctly understand, are you asking me to send you a screenshot of the "Save for Web" feature? If so, see here: https://imgur.com/g87A4ZP (I edited my previous comment) – user2342558 Sep 25 '19 at 07:40
  • No, I want to analyse the actual image produced by GIMP when you export using the parameters shown in the dialog box in your question - not a screenshot of it, but the actual image GIMP writes to disk when you use those options. Thanks. – Mark Setchell Sep 25 '19 at 07:42
  • You can download the original images here: https://cdn.pixabay.com/photo/2012/04/26/19/47/penguin-42936_960_720.png, for the duck https://pixabay.com/photos/animals-bird-duck-drake-curiosity-3708853/ – user2342558 Sep 25 '19 at 07:49
  • No. Please let me have the exact image **that was produced by GIMP** when you did *"Save for Web"* on the penguin picture. – Mark Setchell Sep 25 '19 at 11:50
  • Image sent via email. – user2342558 Sep 25 '19 at 13:36

5 Answers5

5

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().

fmw42
  • 46,825
  • 10
  • 62
  • 80
  • Thanks for your reply, with ImageMagick can I also apply the dithering "Floyd-Steinberg 2"? Pngquant can reduce to 8bit preserving alpha for each pixel with also rare evident banding – user2342558 Sep 21 '19 at 18:21
  • See https://imagemagick.org/Usage/quantize/#dither. `convert image.png -dither FloydSteinberg -colors XX PNG8:dither_floyd.png`, where XX is <=256. Pngquant is likely going to be better as it is specially tuned to optimize PNGs. See https://imagemagick.org/Usage/formats/#png_non-im – fmw42 Sep 21 '19 at 18:32
  • Unfortunatly, I can't use ImageMagick because my code is in a shared hosting without it installed. Anyway thanks for your effort. Can I use ImageMagick without install it in the server? – user2342558 Sep 21 '19 at 21:20
  • If on a Linux server, then check with the hosting provider. ImageMagick usually comes already installed with Linux. – fmw42 Sep 21 '19 at 21:28
  • @user2342558 actually there are some Shared hostings that do offer imagemagick as a PHP extension, check your `phpinfo()` to see if it's enabled. If not you can always ask tech support if it's configured. – Mihail Minkov Sep 21 '19 at 21:32
  • Ask your service provider if it is installed somewhere or to install it. – fmw42 Sep 22 '19 at 18:39
  • They say it cannot be installed, I need to buy their server solution instead of the actual shared hosting. – user2342558 Sep 23 '19 at 06:52
  • Sorry to hear that. If I were you, I would host elsewhere where they do offer ImageMagick with shared hosting. – fmw42 Sep 23 '19 at 17:22
  • I didn't realise PNG8 means only binary transparency. That must be an IM limitation because the PNG specification itself (section 4.2.1.1, tRNS) allows a full 8-bit alpha value to be specified for each palette entry in a color_type 3 image. – Mark Setchell Sep 24 '19 at 20:16
  • Mark, I am unaware of that potential. ImageMagick will not do that in PNG8. But I would be curious to know if any other software will output that on an 8-bit palette type image. ImageMagick had one of the PNG developers (Glenn R-P) on staff and he never mention nor provided for that as an output option in ImageMagick other than what you suggest via tRNS. See https://imagemagick.org/Usage/formats/#png_write and https://imagemagick.org/Usage/formats/#png_formats. Email me separately and we can discuss further. – fmw42 Sep 25 '19 at 01:07
4

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.

Thomas Huijzer
  • 354
  • 1
  • 7
  • Thanks, I tried your code. It reduces the file size preserving the alpha, but if I use a big image (e.g. 300x300px) it create visible big banded areas (e.g. in the belly and under the feet). Your script does not use `imagetruecolortopalette` method, so how can I apply a dithering filter? – user2342558 Sep 24 '19 at 16:10
  • Well I guess it's best to first resize the image and then change the pallet. My script doesn't dither but so doesn't `imagetruecolorpalette`. But maybe you can find some inspirstion from my script. Without something like ImageMagick your options are limited and things like dithering should be done manually. – Thomas Huijzer Sep 24 '19 at 18:21
  • Yes, banding is always the result of reducing colors. As an example I updated my script and added dithering. – Thomas Huijzer Oct 03 '19 at 09:37
  • I see the result with dithering, but it cause the head to be more pixeled than the original. Can you apply the dithering only where there are color bandings? – user2342558 Oct 03 '19 at 09:47
  • Well I don't think this is possible with the Floyd–Steinberg method. But maybe you can implement better dithering algorithms. But reducing colors (quantization) always causes banding. So the only option you have to make it visual more pleasing is dithering. There are a lot of methods for this so you have to choose what fits you best. But I think you will get the best result by first resizing your image and then reducing the colors. – Thomas Huijzer Oct 03 '19 at 09:52
  • Thanks, I'll try a different dithering algorithm. Actually I already first reduce the image before convert it in 8 bit mode. – user2342558 Oct 03 '19 at 09:56
  • Hi Thomas. Can you pls publish againg the source code of your solution? – Massimo Jul 09 '22 at 17:19
4

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
<?php

function 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);

// Palettise
imagetruecolortopalette($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 image

function 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.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • I tried your script but it not works. When $dst is made of `truecolor your script work fine, but id $dts is made of `imagetruecolortopalette` the whole img will be transparent. Am I missing something? Can you please post an example result` for the penguin, using also my code? Thanks – user2342558 Sep 27 '19 at 21:12
  • I am somewhat lost in all the examples of code in your question that don't work and I can't see how you have tried my code. All I can say is, if you have already allocated an index for any transparent colour in your output palletised image, just change the line in my code that says `$trans = imagecolorallocatealpha($im,0,0,0,127);` into `$trans = ` and that should be all you need. – Mark Setchell Sep 29 '19 at 10:47
  • I had some more time to do a complete answer for you - please have another look. – Mark Setchell Sep 30 '19 at 14:03
  • Hi, thanks for your help. I tried your updated answer but, as in your image preview, your script make completely transparent all pixels with an alpha, also if this alpha is very low, see the shadow below the penguin. Also some pixel at the edge of the duck are converted in black pixel or in full-transparent pixel. So it isn't what I need. It little works only if the image has the alpha only all around it and this alpha is total, e.g. the duck. – user2342558 Oct 02 '19 at 14:51
  • If you change the threshold in my code on line 87 of my code to `70`, you will get a larger transparent shadow under the penguin. It has a range of 0..127, I arbitrarily set it at 64 because that is half - you can alter it to whatever you want. – Mark Setchell Oct 02 '19 at 17:42
  • But by doing that, I need to try different threshold for each different image... – user2342558 Oct 02 '19 at 18:12
2

As of yet, I have not found a way to do this exactly short of reimplementing pngquant in PHP/GD, which I think is possible. (That is, quantizing the alpha channel as well. I couldn't get GD to dither alpha in an expected way reliably either.)

However, the following might be a useful middle ground. (For you or others that are stuck with GD.) The resize function accepts a matte color as the background and then sets the pixels that are transparent (or very nearly so) to a transparent index. There's a threshold value to set how much of the alpha to consider. (Lower values for $alphaThreshold would show less of the provided matte color, but remove progressively more of the alpha-transparent sections of the original.)

function resizePng2($originalPath, $xImgNew='', $yImgNew='', $newPath='', $backgroundMatte = [255,255,255], $alphaThreshold = 120)
{
    if(!trim($originalPath) || !$xyOriginalPath = getimagesize("$originalPath")) return false;
    list($xImg, $yImg) = $xyOriginalPath;

    if(!$originalImg = imagecreatefrompng($originalPath)) return false;

    if(!$resizedImg = imagecreatetruecolor($xImgNew, $yImgNew)) return false;
    if(!$refResizedImg = imagecreatetruecolor($xImgNew, $yImgNew)) return false;

    //Fill our resize target with the matte color.
    imagealphablending($resizedImg, true);
    $matte = imagecolorallocatealpha($resizedImg, $backgroundMatte[0], $backgroundMatte[1], $backgroundMatte[2], 0);
    if(!imagefill($resizedImg, 0, 0, $matte)) return false;
    imagesavealpha($resizedImg, true);


    // copy content from originalImg to resizedImg
    if(!imagecopyresampled($resizedImg, $originalImg, 0, 0, 0, 0, $xImgNew, $yImgNew, $xImg, $yImg)) return false;

    //Copy to our reference.
    $refTransparent = imagecolorallocatealpha($refResizedImg, 0, 0, 0, 127);
    if(!imagefill($refResizedImg, 0, 0, $refTransparent)) return false;
    if(!imagecopyresampled($refResizedImg, $originalImg, 0, 0, 0, 0, $xImgNew, $yImgNew, $xImg, $yImg)) return false;

    // PNG-8 bit conversion (Not the greatest, but it does have basic dithering)
    imagetruecolortopalette($resizedImg, true, 255);

    //Allocate our transparent index.
    imagealphablending($resizedImg, true);
    $transparent = imagecolorallocatealpha($resizedImg, 0,0,0,127);

    //Set the pixels in the output image to transparent where they were transparent
    //(or nearly so) in our original image. Set $alphaThreshold lower to adjust affect.
    for($x = 0; $x < $xImgNew; $x++) {
        for($y = 0; $y < $yImgNew; $y++) {
            $alpha = (imagecolorat($refResizedImg, $x, $y) >> 24);
            if($alpha >= $alphaThreshold) {
                imagesetpixel($resizedImg, $x, $y, $transparent);
            }
        }
    }

    if(!imagepng($resizedImg, ($newPath) ?: null, 8)) return false;

    return true;
}

So here would be an example with a white background and a green background. The penguin on the left has a white matte. The penguin on the right has a green matte.

Example results with a white matte and a green matte with extensive drop shadows.

Here's the output with my test penguin:

Test penguin go!


Addendum: So what if you want partially alpha-transparent pixels, but only have GD. You'll need to handle quantizing/dithering yourself. So, as an example: I took a stab at it by building off an existing dithering library and pairing that with my own rudimentary quantizer. (I wouldn't use this in production. At time of writing, the code's a little messy and very untested, and I haven't improved the dithering portion to handle larger palettes so it is VERY slow. [Edit: I added a layer of caching so this is no longer the case, it's now usable for most use cases.])

https://github.com/b65sol/gd-indexed-color-converter

// create an image
$image = imagecreatefrompng('76457185_p0.png');

// create a gd indexed color converter
$converter = new GDIndexedColorConverter();

// the color palette produced by the quantizer phase.
// Could manually add additional colors here.
$palette = $converter->quantize($image, 128, 5);

// THIS IS VERY SLOW! Need to speed up closestColor matching.
// Perhaps with a quadtree.
// convert the image to indexed color mode
$new_image = $converter->convertToIndexedColor($image, $palette, 0.2);

// save the new image
imagepng($new_image, 'example_indexed_color_alpha.png', 8);

Here's an example with alpha transparency preserved in an indexed image:

Example png

EPB
  • 3,939
  • 1
  • 24
  • 26
  • Thanks for your reply. You provided two examples with a matte background, does your function work with transparent background? – user2342558 Sep 27 '19 at 13:48
  • The fully transparent parts are. That's where it's a compromise solution, it's like what we did for save-for-web gifs back in the earlier days. I'll add the white-matte penguin as a test. – EPB Sep 27 '19 at 15:56
  • I did mention that it was a compromise solution. XD The issue is GD's quantizer isn't robust enough. Specifically if you want to maintain partial transparency of the source image GD by itself isn't going to do that. One option might be to dither the alpha channel. (the shadow would become dither'd black dots, and the edges would be sharp/blocky.) The other would be to create your own quantizer that supports partial opacity and do the dithering yourself as I think GD would support outputting such a palette, but it's a lot of work. – EPB Oct 08 '19 at 20:38
  • :) yes. I agree with you. PHP's GD needs some improovments. – user2342558 Oct 09 '19 at 06:22
  • I've added an edit. Unfortunately I don't have the time right now to make it faster/better, but you could use that as a springboard toward your own solution. – EPB Oct 12 '19 at 00:46
-1

as you can see in https://www.php.net/manual/en/function.imagetruecolortopalette.php :

This does not work as well as might be hoped. It is usually best to simply produce a truecolor output image instead, which guarantees the highest output quality.

you can use ImageMagick: https://www.php.net/manual/en/imagick.affinetransformimage.php

Hamid Shariati
  • 546
  • 6
  • 18
  • The quote is obvious. 8 bit is less precise and less quality than 24 bit. And then in the page for Imagick there is a comment which says "This method has not been properly implemented it seems - it has no effect whatsoever on the image." – user2342558 Sep 21 '19 at 17:53
  • Did you have a link about ImageMagick that explain how to do what I'm looking for? – user2342558 Sep 21 '19 at 17:54
  • @user2342558 convert image.png -background white -flatten PNG8:result2.png as fmw42 said. why down point!!! – Hamid Shariati Sep 22 '19 at 13:45