Reputation: 6693
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:
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:
If I set 256 colors instead of 255:
imagetruecolortopalette($resizedImg, true, 256);
the image will be with black background:
A similar result occurs with this image (note the half transparency for the case with 255 colors):
Original: 255 colors: 256 colors:
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:
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()
:
7.0.33
GD
bundled (2.1.0 compatible)PNG Support
enabledlibPNG
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
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.
There are visible color banding in the penguin but the duck its ok (although sometimes the color tone is darker).
Only if the image has only pixels already completely transparent does it work very well (e.g. the duck).
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.
Upvotes: 19
Views: 2982
Reputation: 4029
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.
Here's the output with my test penguin:
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:
Upvotes: 2
Reputation: 364
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
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
Note that dithering also effects the file size because it is 'harder' to compress a PNG when neighboring pixels have different colors.
Upvotes: 4
Reputation: 207335
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
Extracted alpha channel:
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.
And applied the copied alpha to this image.
Where the second image was truecolour, the alpha comes across like this:
Where the second image was palettised, the alpha comes across like this:
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.
Keywords: PHP, gd, image, image processing, alpha, alpha layer, extract alpha, copy alpha, apply alpha, replace alpha.
Upvotes: 4
Reputation: 53081
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:
convert image.png PNG8:result1.png
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
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().
Upvotes: 5
Reputation: 599
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
Upvotes: -1