Проблемы с переносом оболочки PHP/GD в Imagick

Недавно я обнаружил, что Imagick может поддерживать цветовые профили и, таким образом, создавать изображения лучшего качества по сравнению с GD (см. этот вопрос/answer для получения более подробной информации), поэтому я пытаюсь портировать мою оболочку GD, чтобы вместо этого использовать класс Imagick, моя текущая реализация GD выглядит следующим образом:

function Image($input, $crop = null, $scale = null, $merge = null, $output = null, $sharp = true)
{
    if (isset($input, $output) === true)
    {
        if (is_string($input) === true)
        {
            $input = @ImageCreateFromString(@file_get_contents($input));
        }

        if (is_resource($input) === true)
        {
            $size = array(ImageSX($input), ImageSY($input));
            $crop = array_values(array_filter(explode('/', $crop), 'is_numeric'));
            $scale = array_values(array_filter(explode('*', $scale), 'is_numeric'));

            if (count($crop) == 2)
            {
                $crop = array($size[0] / $size[1], $crop[0] / $crop[1]);

                if ($crop[0] > $crop[1])
                {
                    $size[0] = round($size[1] * $crop[1]);
                }

                else if ($crop[0] < $crop[1])
                {
                    $size[1] = round($size[0] / $crop[1]);
                }

                $crop = array(ImageSX($input) - $size[0], ImageSY($input) - $size[1]);
            }

            else
            {
                $crop = array(0, 0);
            }

            if (count($scale) >= 1)
            {
                if (empty($scale[0]) === true)
                {
                    $scale[0] = round($scale[1] * $size[0] / $size[1]);
                }

                else if (empty($scale[1]) === true)
                {
                    $scale[1] = round($scale[0] * $size[1] / $size[0]);
                }
            }

            else
            {
                $scale = array($size[0], $size[1]);
            }

            $image = ImageCreateTrueColor($scale[0], $scale[1]);

            if (is_resource($image) === true)
            {
                ImageFill($image, 0, 0, IMG_COLOR_TRANSPARENT);
                ImageSaveAlpha($image, true);
                ImageAlphaBlending($image, true);

                if (ImageCopyResampled($image, $input, 0, 0, round($crop[0] / 2), round($crop[1] / 2), $scale[0], $scale[1], $size[0], $size[1]) === true)
                {
                    $result = false;

                    if ((empty($sharp) !== true) && (is_array($matrix = array_fill(0, 9, -1)) === true))
                    {
                        array_splice($matrix, 4, 1, (is_int($sharp) === true) ? $sharp : 16);

                        if (function_exists('ImageConvolution') === true)
                        {
                            ImageConvolution($image, array_chunk($matrix, 3), array_sum($matrix), 0);
                        }
                    }

                    if ((isset($merge) === true) && (is_resource($merge = @ImageCreateFromString(@file_get_contents($merge))) === true))
                    {
                        ImageCopy($image, $merge, round(0.95 * $scale[0] - ImageSX($merge)), round(0.95 * $scale[1] - ImageSY($merge)), 0, 0, ImageSX($merge), ImageSY($merge));
                    }

                    foreach (array('gif' => 0, 'png' => 9, 'jpe?g' => 90) as $key => $value)
                    {
                        if (preg_match('~' . $key . '$~i', $output) > 0)
                        {
                            $type = str_replace('?', '', $key);
                            $output = preg_replace('~^[.]?' . $key . '$~i', '', $output);

                            if (empty($output) === true)
                            {
                                header('Content-Type: image/' . $type);
                            }

                            $result = call_user_func_array('Image' . $type, array($image, $output, $value));
                        }
                    }

                    return (empty($output) === true) ? $result : self::Chmod($output);
                }
            }
        }
    }

    else if (count($result = @GetImageSize($input)) >= 2)
    {
        return array_map('intval', array_slice($result, 0, 2));
    }

    return false;
}

Я экспериментировал с методами класса Imagick, и это то, что я получил до сих пор:

function Imagick($input, $crop = null, $scale = null, $merge = null, $output = null, $sharp = true)
{
    if (isset($input, $output) === true)
    {
        if (is_file($input) === true)
        {
            $input = new Imagick($input);
        }

        if (is_object($input) === true)
        {
            $size = array_values($input->getImageGeometry());
            $crop = array_values(array_filter(explode('/', $crop), 'is_numeric'));
            $scale = array_values(array_filter(explode('*', $scale), 'is_numeric'));

            if (count($crop) == 2)
            {
                $crop = array($size[0] / $size[1], $crop[0] / $crop[1]);

                if ($crop[0] > $crop[1])
                {
                    $size[0] = round($size[1] * $crop[1]);
                }

                else if ($crop[0] < $crop[1])
                {
                    $size[1] = round($size[0] / $crop[1]);
                }

                $crop = array($input->getImageWidth() - $size[0], $input->getImageHeight() - $size[1]);
            }

            else
            {
                $crop = array(0, 0);
            }

            if (count($scale) >= 1)
            {
                if (empty($scale[0]) === true)
                {
                    $scale[0] = round($scale[1] * $size[0] / $size[1]);
                }

                else if (empty($scale[1]) === true)
                {
                    $scale[1] = round($scale[0] * $size[1] / $size[0]);
                }
            }

            else
            {
                $scale = array($size[0], $size[1]);
            }

            $image = new IMagick();
            $image->newImage($scale[0], $scale[1], new ImagickPixel('white'));

            $input->cropImage($size[0], $size[1], round($crop[0] / 2), round($crop[1] / 2));
            $input->resizeImage($scale[0], $scale[1], Imagick::FILTER_LANCZOS, 1); // $image->scaleImage($scale[0], $scale[1]);

            //if (in_array('icc', $image->getImageProfiles('*', false)) === true)
            {
                $version = preg_replace('~([^-]*).*~', '$1', ph()->Value($image->getVersion(), 'versionString'));

                if (is_file($profile = sprintf('/usr/share/%s/config/sRGB.icm', str_replace(' ', '-', $version))) !== true)
                {
                    $profile = 'http://www.color.org/sRGB_v4_ICC_preference.icc';
                }

                if ($input->profileImage('icc', file_get_contents($profile)) === true)
                {
                    $input->setImageColorSpace(Imagick::COLORSPACE_SRGB);
                }
            }

            $image->compositeImage($input, Imagick::COMPOSITE_OVER, 0, 0);

            if ((isset($merge) === true) && (is_object($merge = new Imagick($merge)) === true))
            {
                $image->compositeImage($merge, Imagick::COMPOSITE_OVER, round(0.95 * $scale[0] - $merge->getImageWidth()), round(0.95 * $scale[1] - $merge->getImageHeight()));
            }

            foreach (array('gif' => 0, 'png' => 9, 'jpe?g' => 90) as $key => $value)
            {
                if (preg_match('~' . $key . '$~i', $output) > 0)
                {
                    $type = str_replace('?', '', $key);
                    $output = preg_replace('~^[.]?' . $key . '$~i', '', $output);

                    if (empty($output) === true)
                    {
                        header('Content-Type: image/' . $type);
                    }

                    $image->setImageFormat($type);

                    if (strcmp('jpeg', $type) === 0)
                    {
                        $image->setImageCompression(Imagick::COMPRESSION_JPEG);
                        $image->setImageCompressionQuality($value);
                        $image->stripImage();
                    }

                    if (strlen($output) > 0)
                    {
                        $image->writeImage($output);
                    }

                    else
                    {
                        echo $image->getImageBlob();
                    }
                }
            }

            return (empty($output) === true) ? $result : self::Chmod($output);
        }
    }

    else if (count($result = @GetImageSize($input)) >= 2)
    {
        return array_map('intval', array_slice($result, 0, 2));
    }

    return false;
}

Базовая функциональность (обрезка/изменение размера/водяной знак) уже поддерживается, однако у меня все еще есть некоторые проблемы. Поскольку документация по PHP Imagick выглядит отстойной, у меня нет другого выбора, кроме как попробовать сочетание проб и ошибок всех доступных методов и аргументов, что занимает много времени.

Мои текущие проблемы/сомнения:


1 - Сохранение прозрачности

В моей первоначальной реализации строки:

ImageFill($image, 0, 0, IMG_COLOR_TRANSPARENT);
ImageSaveAlpha($image, true);
ImageAlphaBlending($image, true);

Имейте эффект сохранения прозрачности, когда вы конвертируете прозрачное изображение PNG в выход PNG. Если, однако, вы пытаетесь преобразовать прозрачное изображение PNG в формат JPEG, прозрачные пиксели должны иметь свой цвет в белый. Пока что с ImageMagick я смог преобразовать все прозрачные пикселы в белый, но я не могу сохранить прозрачность, если формат вывода поддерживает его.


2 - Сжатие выходных форматов (а именно JPEG и PNG)

Моя первоначальная реализация использует уровень сжатия 9 на PNG и качество 90 в JPEG:

foreach (array('gif' => 0, 'png' => 9, 'jpe?g' => 90) as $key => $value)

Строки:

$image->setImageCompression(Imagick::COMPRESSION_JPEG);
$image->setImageCompressionQuality($value);
$image->stripImage();

Похоже на сжатие изображений JPEG - однако GD может сжимать его гораздо больше, используя тот же $value, что и аргумент качества - почему? Я также в неведении относительно различий между:

Какой я должен использовать и каковы их отличия? Кроме того, самая важная проблема связана с сжатием PNG, список констант сжатия Imagick, похоже, не поддерживает форматы PNG:

imagick::COMPRESSION_UNDEFINED (integer)
imagick::COMPRESSION_NO (integer)
imagick::COMPRESSION_BZIP (integer)
imagick::COMPRESSION_FAX (integer)
imagick::COMPRESSION_GROUP4 (integer)
imagick::COMPRESSION_JPEG (integer)
imagick::COMPRESSION_JPEG2000 (integer)
imagick::COMPRESSION_LOSSLESSJPEG (integer)
imagick::COMPRESSION_LZW (integer)
imagick::COMPRESSION_RLE (integer)
imagick::COMPRESSION_ZIP (integer)
imagick::COMPRESSION_DXT1 (integer)
imagick::COMPRESSION_DXT3 (integer)
imagick::COMPRESSION_DXT5 (integer)

Это краска в заднице, поскольку вывод GD PNG, размер которого составляет 100-200 КБ, становится чрезвычайно толще, если вместо него выводится Imagick (размер порядка 2 МБ)...

Есть пара вопросов на SO относительно этой проблемы, но я не смог найти какое-либо рабочее решение, которое не полагается на внешние приложения. Это действительно невозможно сделать с ImageMagick?!


3 - Конволюции изображений

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


4 - Цветные профили

Это не связано с первоначальной реализацией, но я также хотел бы, чтобы все было правильно.

Должен ли я всегда добавлять цветовой профиль sRGB Imagick/International Color Consortium ко всем изображениям? Или это должно быть добавлено только тогда, когда есть (или нет) конкретный цветовой профиль?

Кроме того, следует ли удалить существующие цветовые профили?

Я понимаю, что это может быть широкий вопрос, но мое понимание цветовых профилей очень ограничено, и некоторые общие рекомендации по этому вопросу будут очень оценены.


5 - Открытие удаленных изображений

GD изначально поддерживает открытие удаленных изображений либо с помощью функций ImageCreateFrom*, либо с помощью file_get_contents() в сочетании с ImageCreateFromString(), как я делаю.

Imagick, похоже, может открывать только локальные изображения или открывать дескрипторы файлов. Есть ли простой способ заставить Imagick читать удаленные изображения (без необходимости открывать и закрывать файлы)?


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

Спасибо заранее!

Ответы

Ответ 1

Существует ряд причин, по которым ваши PNG-изображения могут увеличиваться по размеру, наиболее очевидным из которых вы столкнетесь, является невозможность передачи GM/IM прозрачности в виде фрагмента tRNS (в основном, булевская прозрачность для изображений PNG). К сожалению, разработчики GraphicsMagick и ImageMagick еще не реализовали эту функцию. Я обменялся с ними электронными письмами, поэтому я точно знаю это.

Я знаю, что вы не хотите использовать внешние инструменты, но доверьтесь мне. Изображение /GraphicsMagick действительно плохо при сжатии изображений PNG. Решение, которое я использую, использует GraphicsMagick для управления изображением, а также проверяет, содержит ли изображение прозрачные пиксели, если оно содержит прозрачные пиксели, затем запустите OptiPNG на изображении. OptiPNG увидит, что прозрачность может передаваться как часть tRNS и действовать соответственно. На самом деле вы должны запускать OptiPNG на всех изображениях PNG после использования Image/GraphicsMagick, потому что я обнаружил, что вы можете добиться гораздо большего сжатия. Вы также можете сэкономить место, отключив сглаживание и используя цветовое пространство YUV.

Что касается GM, уменьшающего размер изображений лучше, чем IM, вы должны знать, что GM по умолчанию использует 8-битное цветовое пространство, когда цвет уменьшает изображения, а ImageMagick по умолчанию использует 16 бит. Вот почему GM намного быстрее, чем IM, когда цвет уменьшает изображения до значения более 255 цветов. Возможно, вам нужно проверить количество цветов на каждом изображении после сжатия для подтверждения.

Ответ 2

Вы можете использовать optipng (еще один инструмент командной строки PNG), чтобы оптимизировать размер ваших файлов PNG.

Ответ 3

Поскольку не поддерживает ICM в браузерах, профили по сути являются пустой тратой пропускной способности. Таким образом, если ваши изображения находятся в sRGB, вы можете безопасно уничтожить профиль, иначе лучше преобразовать изображение в sRGB и затем удалить его профиль.

Причиной удаления профиля изображений sRGB является то, что sRGB является стандартом в Интернете, на компьютерах и принтерах, и даже Firefox применяет цветовой профиль sRGB для немаркированные изображения.

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

Нижняя строка: цветные профили являются злыми. Используйте их только в том случае, если они вам действительно нужны.

То, что я сказал, прав, только если вы нацеливаете свой сайт на самую широкую аудиторию. В противном случае YMMV.

Ответ 4

Я не уверен, что вам все еще нужен ответ, но я пишу библиотеку обработки изображений, которая обертывает GD и Imagick, поэтому я столкнулся с некоторыми из ваших проблем.

2 - Сжатие выходных форматов (а именно JPEG и PNG)

ImageMagick не обеспечивает сжатие для PNG для простого факта, что PNG представляет собой формат без потерь в отличие от JPEG, который является "потерянным" форматом. Я бы сказал, что ImageMagick правильно понял это.

Просто отбросьте параметр для сжатия в PNG и просто поставьте стандартное значение по умолчанию для imagepng в GD.

3 - Конволюции изображений

Просто зациклируйте на каждом пикселе с помощью getPixelIterator и выполните ручную свертку. Wikipedia имеет хорошую статью об этом с псевдокодом.

5 - Открытие удаленных изображений

Вы можете открыть изображение отдельно и передать его в Imagick

$handle = fopen('http://example.com/foo.jpg', 'rb');
$img = new Imagick();
$img->readImageFile($handle);