Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP wrong result for imagetruecolortopalette with PNG with transparency

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.

like image 548
user2342558 Avatar asked Sep 18 '19 11:09

user2342558


2 Answers

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

like image 182
fmw42 Avatar answered Nov 07 '22 22:11

fmw42


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.

like image 4
Thomas Huijzer Avatar answered Nov 07 '22 23:11

Thomas Huijzer