Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Issues porting PHP/GD wrapper to Imagick

I've recently discovered that Imagick can support color profiles and thus produce images of better quality compared to GD (see this question / answer for more details), so I'm trying to port my GD wrapper to use the Imagick class instead, my current GD implementation looks like this:

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;
}

I've been experimenting with the Imagick class methods and this is what I got so far:

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;
}

The basic functionality (crop / resize / watermark) is already supported, however, I'm still having some issues. Since the PHP Imagick documentation kinda sucks I've no other choice than to try a trial and error approach combination of all the available methods and arguments, which takes a lot of time.

My current problems / doubts are:


1 - Preserving Transparency

In my original implementation, the lines:

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

Have the effect of preserving the transparency when you are converting a transparent PNG image to a PNG output. If, however, you try to convert a transparent PNG image to a JPEG format, the transparent pixels should have their color set to white. So far, with ImageMagick, I've only been able to convert all transparent pixels to white, but I can't preserve the transparency if the output format supports it.


2 - Compressing Output Formats (Namely JPEG and PNG)

My original implementation uses a compression level of 9 on PNGs and a quality of 90 on JPEGs:

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

The lines:

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

Seem to compress JPEG images - GD however, is able to compress it much more using the same $value as a quality argument - why? I'm also in the dark regarding the differences between:

  • Imagick::setCompression() / Imagick::setImageCompression() and
  • Imagick::setCompressionQuality() / Imagick::setImageCompressionQuality()

Which one should I use and what are their differences? Also, the most critical problem has to do with PNG compression, the list of Imagick compression constants seem to not support PNG formats:

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)

This is being a paint in the ass, since a GD PNG output that happens to have a size of 100-200 KB gets extremely fatter if outputted with Imagick instead (size in the order of 2 MB)...

There are a couple of questions on SO regarding this issue, but I haven't been able to find any working solution that doesn't rely on external applications. Is this really impossible to do with ImageMagick?!


3 - Image Convolutions

In the GD implementation I call ImageConvolution() to sharpen the image a bit, I know that Imagick has built-in methods to sharpen images (I haven't had the chance to try them out yet) but I'd like to know if Imagick has an equivalent of the ImageConvolution() function.


4 - Color Profiles

This is not related to the original implementation, but I would also like to get it right.

Should I always add the Imagick / International Color Consortium sRGB color profile to all images? Or should this only be added when there is (or isn't) a specific color profile?

Also, should I delete the existing color profiles?

I understand that this may be a broad question but my understanding of color profiles is very limited and some general guidance on this would be very much appreciated.


5 - Opening Remote Images

GD natively supports opening remote images, either via the ImageCreateFrom* functions, or using file_get_contents() in combination with ImageCreateFromString() like I am doing.

Imagick seems to only be able to open local images, or open file handles. Is there any straightforward way to make Imagick read remote images (without having to open and close file handles)?


If someone could shed some light into any of these questions I will be very grateful.

Thanks in advance!

like image 800
Alix Axel Avatar asked Apr 28 '11 12:04

Alix Axel


3 Answers

There are a number of reasons why your PNG images may be increasing in size, the most obvious one that you will run into is GM/IM's inability to convey transparency as a tRNS chunk (basically boolean transparency for PNG images). Unfortunately the maintainers of GraphicsMagick and ImageMagick have not implemented this feature yet. I exchanged emails with them so I know this for sure.

I know you don't want to use external tools but trust me you do. Image/GraphicsMagick are really bad at compressing PNG images. The solution I am using is, use GraphicsMagick to manipulate the image and also check if the image contains transparent pixels, if it does contain transparent pixels then run OptiPNG on the image. OptiPNG will see that transparency can be conveyed as a tRNS chunk and act accordingly. Actually you should run OptiPNG on all PNG images after using Image/GraphicsMagick because I have found that you can achieve much greater compression. You can also save space by turning dithering off and by using the YUV color space.

As for GM reducing the size of images better than IM, you should know that GM by default uses an 8 bit color space when color reducing images while ImageMagick by default uses 16 bits. This is why GM is so much faster than IM when color reducing images to a value over 255 colors. Maybe you should check the number of colors in each image after compression to confirm.

like image 121
toc777 Avatar answered Nov 16 '22 14:11

toc777


You can use optipng (another PNG command-line tool) to optimize the size of your PNG files.

like image 35
Mathieu Rodic Avatar answered Nov 16 '22 14:11

Mathieu Rodic


As there is not much support for ICM in browsers the profiles are essentially a waste of bandwidth. Thus if your images are in sRGB you can safely trash the profile, otherwise it is better to convert an image into sRGB and trash its profile afterwards.

The reason for removing a profile of sRGB images is that sRGB is effectively a standard on the Internet, on computers, and on printers, and even Firefox applies sRGB color profile to untagged images.

There is another reason for removing all profiles altogether, thou I'm not sure if it applies to your case: if you're planning to mix images with embedded profiles with other profile-less images, e.g. GIF images, which cannot contain a profile by definition, you'll end up with a messy result on an ICC-enabled browser. It'll render some images as per their embedded color space and other with a some other color profile, which leads to a situation in which you'll see a boundary between an image with an embedded ICC profile with a solid background color adjoining other profile-less image with the same color background color. Even if you manage to get a profile for every image on your page, there are a lot of users who use ancient ICC-disabled browsers.

Bottom line: color profiles are evil. Only use them if you actually need them.

What I said is right only if you target your site for a widest audience possible. Otherwise YMMV.

like image 1
sanmai Avatar answered Nov 16 '22 16:11

sanmai