PHP неверный результат для imagetruecolortopalette с PNG с прозрачностью

Я пытаюсь написать скрипт, который изменяет размер изображения PNG, а затем преобразует его в битовый режим PNG-8. Таким образом, размер полученного файла будет меньше, но без потери качества.

Изменение: стиль цитаты для изображений, чтобы лучше показать прозрачность

Изменение размера отлично работает, сохраняя прозрачность:

originalImage

Проблема в том, что я конвертирую изображение в 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);

В результате получается изображение с прозрачностью вокруг и немного внутри изображения:

enter image description here

Если я установлю 256 цветов вместо 255:

imagetruecolortopalette($resizedImg, true, 256);

изображение будет с черным фоном:

enter image description here

Аналогичный результат происходит с этим изображением (обратите внимание на полупрозрачность для случая с 255 цветами):

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

Полный код функции:

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;
    

Результаты:

enter image description here enter image description here

ничего не меняется

  • другие ТАК сообщения и некоторые в Интернете

Также без изменения размера изображения (удаление 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

enter image description here enter image description here

и это приводит к уменьшенному изображению, почти идентичному оригиналу.

Также pngquant, tinypng и многие другие выполняют ту же работу, но мне нужно сделать это с помощью PHP.

Edit2:

К сожалению, я не могу использовать ImageMagick, потому что мой код находится на общем хостинге без его установки.

Edit3:

в phpinfo() приводит к тому, что модуль imagemagick не установлен.

Edit4:

Срок действия награды истекает, в следующие дни позвольте мне провести несколько тестов с вашими ответами, возможно, есть решение только с PHP.

Edit5:

Это мои попытки с вашими ответами (обновлено на 2019-10-02).

Примечание. Я поместил базовую сетку, чтобы лучше показать альфа.

Томас Хуйцзер ответ:

enter image description here enter image description here

У пингвина видны цветные полосы, но утка в порядке (хотя иногда цветовой тон темнее).

Ответ EPB:

enter image description here enter image description here

Только если изображение имеет только пиксели, уже полностью прозрачные, оно работает очень хорошо (например, утка).

Марк Сетчелл ответит:

enter image description here enter image description here

Это делает полностью прозрачными все пиксели с альфа-каналом, также, если эта альфа очень низкая, см. тень под пингвином. Также некоторые пиксели на краю утки преобразуются в черный пиксель или в полностью прозрачный пиксель.

Ответы

Ответ 1

Вы можете сделать это довольно легко в ImageMagick, который распространяется в Linux и доступен для Windows и Mac OSX. Есть также много API, кроме командной строки. Вот как это сделать в командной строке ImageMagick.

Вход:

enter image description here

convert image.png PNG8:result1.png


enter image description here

PNG8: означает 256 цветов и двоичную прозрачность. Это означает либо полную, либо отсутствие прозрачности. Это вызывает алиасинг (ступеньки) по краям. Если вы хотите установить цвет фона вместо прозрачности, вы можете сохранить плавный (сглаженный) контур в результате. Так что для белого фона.

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


enter image description here

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

PNG reduced to 32 colors

Правка 2: Я обновил свой сценарий и добавил необязательное сглаживание Флойда-Стейнберга. Результат с 16 цветами на канал:

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

enter image description here

Обратите внимание, что сглаживание также влияет на размер файла, поскольку сжать 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;
}

Итак, вот пример с белым фоном и зеленым фоном. Пингвин слева имеет белый матовый цвет. Пингвин справа имеет зеленый матовый цвет.

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

Вот вывод с моим тестовым пингвином:

Test penguin go!


Приложение: Так что, если вы хотите частично альфа-прозрачные пиксели, но иметь только 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);

Вот пример с альфа-прозрачностью, сохраненной в индексированном изображении:

Example png

Ответ 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');
?>

Результат

enter image description here

Извлеченный альфа-канал:

enter image description here

Оригинальный ответ

Я создал функцию PHP, чтобы извлечь альфа-канал из изображения и затем применить этот альфа-канал к другому изображению.

Если вы примените скопированный альфа-канал к истинному цветному изображению, это позволит получить плавную альфа-канал с 7-битным разрешением, т.е. до 127. Если вы примените скопированный альфа-канал к палитризованному изображению, пороговое значение будет 50% (вы можете измените его), чтобы выходное изображение имело двоичную (вкл/выкл) альфа.

Итак, я извлек альфа из этого изображения - надеюсь, вы увидите, что в середине есть альфа рампа/градиент.

enter image description here

И применил скопированную альфу к этому изображению.

enter image description here

Там, где второе изображение было истинным цветом, альфа выглядит так:

enter image description here

Там, где второе изображение было в палитре, альфа выглядит так:

enter image description here

Код должен быть довольно понятным. Раскомментируйте операторы 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');
?>

Вот извлеченный альфа-слой, просто для удовольствия. Обратите внимание, что на самом деле это изображение в оттенках серого, представляющее альфа-канал - оно не имеет альфа-компонента.

enter image description here

Ключевые слова: PHP, gd, изображение, обработка изображений, альфа, альфа-слой, извлечение альфы, копирование альфы, применение альфы, замена альфы.

Ответ 5

как вы можете видеть в https://www.php.net/manual/en/function.imagetruecolortopalette.php :

Это не работает так, как можно надеяться. Обычно лучше всего вместо этого просто создайте изображение с истинным цветом, что гарантирует высочайшее качество продукции.

Вы можете использовать ImageMagick: https://www.php.net/manual/en/imagick.affinetransformimage.php