PHP неверный результат для imagetruecolortopalette с PNG с прозрачностью
Я пытаюсь написать скрипт, который изменяет размер изображения PNG, а затем преобразует его в битовый режим PNG-8. Таким образом, размер полученного файла будет меньше, но без потери качества.
Изменение: стиль цитаты для изображений, чтобы лучше показать прозрачность
Изменение размера отлично работает, сохраняя прозрачность:
Проблема в том, что я конвертирую изображение в 8 бит:
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);
В результате получается изображение с прозрачностью вокруг и немного внутри изображения:
Если я установлю 256 цветов вместо 255:
imagetruecolortopalette($resizedImg, true, 256);
изображение будет с черным фоном:
Аналогичный результат происходит с этим изображением (обратите внимание на полупрозрачность для случая с 255 цветами):
original: 255 colors: 256 colors:
Полный код функции:
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;
}
Что я пробовал:
qaru.site/info/1740766/...
// 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;
Результаты:
ничего не меняется
- другие ТАК сообщения и некоторые в Интернете
Также без изменения размера изображения (удаление imagecopyresampled
и адаптация имени переменной) результат будет таким же.
Не могли бы вы помочь мне заставить его работать и понять причину этого странного поведения?
Некоторая информация в phpinfo()
:
- PHP
7.0.33
GD
в комплекте (совместимо с 2.1.0)
PNG Support
включен
libPNG
1.5.13.
Изменить:
В GIMP v.2.8.22 я могу сохранить изображение для Web со следующими свойствами:
PNG-8
256 colors palette
Dither: Floyd-Steinberg / Floyd-Steinberg 2 / positioned
и это приводит к уменьшенному изображению, почти идентичному оригиналу.
Также pngquant, tinypng и многие другие выполняют ту же работу, но мне нужно сделать это с помощью PHP.
Edit2:
К сожалению, я не могу использовать ImageMagick, потому что мой код находится на общем хостинге без его установки.
Edit3:
в phpinfo()
приводит к тому, что модуль imagemagick
не установлен.
Edit4:
Срок действия награды истекает, в следующие дни позвольте мне провести несколько тестов с вашими ответами, возможно, есть решение только с PHP.
Edit5:
Это мои попытки с вашими ответами (обновлено на 2019-10-02).
Примечание. Я поместил базовую сетку, чтобы лучше показать альфа.
Томас Хуйцзер ответ:
У пингвина видны цветные полосы, но утка в порядке (хотя иногда цветовой тон темнее).
Ответ EPB:
Только если изображение имеет только пиксели, уже полностью прозрачные, оно работает очень хорошо (например, утка).
Марк Сетчелл ответит:
Это делает полностью прозрачными все пиксели с альфа-каналом, также, если эта альфа очень низкая, см. тень под пингвином. Также некоторые пиксели на краю утки преобразуются в черный пиксель или в полностью прозрачный пиксель.
Ответы
Ответ 1
Вы можете сделать это довольно легко в ImageMagick, который распространяется в Linux и доступен для Windows и Mac OSX. Есть также много API, кроме командной строки. Вот как это сделать в командной строке ImageMagick.
Вход:
convert image.png PNG8:result1.png
PNG8: означает 256 цветов и двоичную прозрачность. Это означает либо полную, либо отсутствие прозрачности. Это вызывает алиасинг (ступеньки) по краям. Если вы хотите установить цвет фона вместо прозрачности, вы можете сохранить плавный (сглаженный) контур в результате. Так что для белого фона.
convert image.png -background white -flatten PNG8:result2.png
ImageMagick работает под управлением PHP Imagick. Так что вы должны быть в состоянии сделать это с помощью PHP Imagick. Или вы можете вызвать командную строку ImageMagick из PHP exec().
Ответ 2
Я не думаю, что это странное поведение.
Документация PHP не говорит об этом, но я предполагаю, что imagefill()
работает так же, как и в большинстве других приложений: путем заполнения связанных пикселей тем же цветом, что и пиксель, с которого началось заполнение (0, 0)
.
Поскольку сначала вы устанавливаете палитру на 255 пикселей (или 256), вы преобразуете все темные области в черный цвет и теряете всю прозрачность.
Когда вы заливаете заливку, начиная с левой верхней части, все подключенные пиксели (также внутри пингвина и утки) станут прозрачными.
Я думаю, что единственный способ сделать это без ImageMagick - это пройти все пиксели изображения с измененным размером и вручную установить цвет пикселя для ограниченного поддона.
Некоторое время назад я написал небольшой скрипт, который уменьшает цвета PNG, сохраняя полную альфа-информацию (1).
Это уменьшит размер файла PNG и, следовательно, размер файла. Не имеет большого значения, будет ли результирующий PNG больше 8 бит. Небольшой поддон в любом случае уменьшит размер файла.
(1) https://bitbucket.org/thuijzer/pngreduce/
Изменить: Я просто использовал ваш PNG с измененным размером (с прозрачностью) в качестве входных данных для моего сценария и преобразовал его из файла размером 12 КБ в файл размером 7 КБ, используя только 32 цвета:
Reduced to 62.28% of original, 12.1kB to 7.54kB
Правка 2: Я обновил свой сценарий и добавил необязательное сглаживание Флойда-Стейнберга.
Результат с 16 цветами на канал:
Reduced to 66.94% of original, 12.1kB to 8.1kB
Обратите внимание, что сглаживание также влияет на размер файла, поскольку сжать PNG "сложнее", когда соседние пиксели имеют разные цвета.
Ответ 3
На данный момент я не нашел способа сделать это, если не считать переопределения pngquant в PHP/GD, что, я думаю, возможно. (То есть квантование также и альфа-канала. Я также не смог заставить GD надежно сглаживать альфу ожидаемым образом.)
Тем не менее, следующее может быть полезным промежуточным пунктом. (Для вас или других людей, которые застряли в GD.) Функция изменения размера принимает матовый цвет в качестве фона, а затем устанавливает прозрачные (или почти почти) пиксели в
прозрачный индекс. Существует пороговое значение, чтобы установить, сколько альфа нужно учитывать. (Более низкие значения для $alphaThreshold
будут показывать меньше предоставленного матового цвета, но будут постепенно удалять больше альфа-прозрачных участков оригинала.)
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;
}
Итак, вот пример с белым фоном и зеленым фоном. Пингвин слева имеет белый матовый цвет. Пингвин справа имеет зеленый матовый цвет.
Вот вывод с моим тестовым пингвином:
Приложение: Так что, если вы хотите частично альфа-прозрачные пиксели, но иметь только GD. Вы должны будете справиться с квантованием/дизерингом самостоятельно. Итак, в качестве примера: я взял удар, построив существующую библиотеку сглаживания и соединив ее с моим собственным элементарным квантователем. (Я бы не стал использовать это в работе. На момент написания код немного запутанный и очень непроверенный, и я не улучшил часть сглаживания для обработки больших палитр, поэтому она ОЧЕНЬ медленная. [Редактировать: я добавил слой кэширования, так что это больше не так, теперь его можно использовать в большинстве случаев.])
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);
Вот пример с альфа-прозрачностью, сохраненной в индексированном изображении:
Ответ 4
Обновленный ответ
У меня было немного больше времени, чтобы разработать полный код, чтобы ответить вам - я значительно упростил то, что у вас было, и, похоже, теперь он делает то, что вы хотите!
#!/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');
?>
Результат
Извлеченный альфа-канал:
Оригинальный ответ
Я создал функцию PHP, чтобы извлечь альфа-канал из изображения и затем применить этот альфа-канал к другому изображению.
Если вы примените скопированный альфа-канал к истинному цветному изображению, это позволит получить плавную альфа-канал с 7-битным разрешением, т.е. до 127. Если вы примените скопированный альфа-канал к палитризованному изображению, пороговое значение будет 50% (вы можете измените его), чтобы выходное изображение имело двоичную (вкл/выкл) альфа.
Итак, я извлек альфа из этого изображения - надеюсь, вы увидите, что в середине есть альфа рампа/градиент.
И применил скопированную альфу к этому изображению.
Там, где второе изображение было истинным цветом, альфа выглядит так:
Там, где второе изображение было в палитре, альфа выглядит так:
Код должен быть довольно понятным. Раскомментируйте операторы printf()
, содержащие DEBUG:
, для большого количества вывода:
#!/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');
?>
Вот извлеченный альфа-слой, просто для удовольствия. Обратите внимание, что на самом деле это изображение в оттенках серого, представляющее альфа-канал - оно не имеет альфа-компонента.
Ключевые слова: PHP, gd, изображение, обработка изображений, альфа, альфа-слой, извлечение альфы, копирование альфы, применение альфы, замена альфы.
Ответ 5
как вы можете видеть в https://www.php.net/manual/en/function.imagetruecolortopalette.php :
Это не работает так, как можно надеяться. Обычно лучше всего вместо этого просто создайте изображение с истинным цветом, что гарантирует высочайшее качество продукции.
Вы можете использовать ImageMagick: https://www.php.net/manual/en/imagick.affinetransformimage.php