Howto: Rotate and Flip images in PHP using GD, retaining PNG transparency

Rotate and Flip images in PHP
This entry is part 2 of 2 in the series PHP Image Manipulation

Rotate and Flip images in PHPSo, this is the second of the PHP Image Manipulation series and today, we are going to talk about how to rotate and flip images in PHP using predefined GD library functions. Rotation and flipping is perhaps one of the most important image manipulation techniques. Using a few library function call, it is really simple to do such manipulation in PHP. Also, we shall look into the transparency for PNG images. While doing the rotation and flipping if we do not explicitly manage the alpha values, then it loses its transparency. We shall see how not to make that happen.

At the end of this tutorial, you should be able to do:

  • Flipping: Horizontal, Vertical and Both.
  • Rotation: There will be no constrain in the rotation method. We shall be able to pass the rotation angle (clockwise rotation for easier understanding with the real life) and background color with transparency.
  • Saving modified image in a file: This additional topic is necessary as it is completely useless without saving the output.
  • Direct output to the browser: We shall also see how to output the image directly into the browser by modifying the header message.

It recommended that you read the tutorial published before in order to understand the saving of images. Before we start our discussion, here are (as usual):

#1: Analyzing the concept:

#1.1: Image flipping:

There is no direct library method to do image flipping. But it can be done with a little bit of help from mathematics. For example, let us consider we want to flip an image vertically. We can follow the algorithm below:

  • Break the image into rows of pixels from x-ordinate 0 (left) to end (right).
  • Interchange the rows from top to bottom, ie, the top row goes to the bottom row and vice-versa.
  • Do the same thing until all the rows of the image gets interchanged.

Sounds simple enough? Now, for horizontal flipping we can do:

  • Break the images into column of pixes from y-ordinate 0 (top) to end (bottom).
  • Interchange the columns from left to right.

In practice:

  • We shall create a dummy image of same  height and width.
  • Copy the rows or columns to new position.
  • Save the new image.

#1.2: Image rotation:

In GD, we have a library function imagerotate which can rotate an image based on angle and background color. But the main pitfall is, it rotates the image in anti-clock direction and we must create the background color instead of passing some hexadecimal color values from any colorpicker. So, we shall make our method a bit advance which would solve both these drawbacks.

#2: Writing the source code:

#2.1: The main class architecture:

Keeping in mind the above strategies, we start by creating our own class, ImageManipulation:

class ImageManipulation {

    /**
     * The image container variable
     * Should be false if no image present
     * Otherwise should hold the GD Image Object
     * @var &gd.return.identifier
     */
    public $img;

    /**
     * The type of the image loaded
     * Should be false or any of the jpg|jpeg|png|gif
     * @var string
     */
    public $type;
    /*...*/
}

So, it has got two variables:

  • img : Holds the image object we are dealing with.
  • type : References the type of the image (jpeg, png, gif) by storing the extension.

For the problems we are dealing with right now, we shall create 5 public methods, load, save_image, output, flip_image and rotate_image. There will also be some private methods and constructor.

Let us quickly see the prototypes of these methods:

    public function load($filename) {}

    public function save_image($filename, $type = 'inherit', $jpeg_quality = 100, $png_compression = 0) {}

    public function output($type = 'inherit', $jpeg_quality = 100, $png_compression = 0) {}

    public function flip_image($type) {}

    public function rotate_image($angle, $bgd_color = 'ffffff', $alpha = 0) {}

#2.2: The load method:

    /**
     * The loader function
     * Loads the image from a file to the $this->img variable
     *
     * Also checks if the file exists and of valid image type by checking the extension
     * @param string $filename The path of the file to be loaded
     * @return boolean true on success, false otherwise
     */
    public function load($filename) {
        //first check if the file exists
        if(!file_exists($filename))
            return false;

        //extract the extension & thereby check if valid image
        $ext = '';
        //we check the extension, it can be 4 characters (for jpeg) and must be atleast 3 characters
        if(preg_match('/.*\.([A-Za-z]?[A-Za-z]{3})$/', $filename, $ext)) {
            //var_dump($ext);
            $ext = $ext[1];
            //var_dump($ext);
        } else {
            //invalid image
            return false;
        }

        $this->type = $ext;
        switch($ext) {
            case 'jpg' :
            case 'jpeg' :
                $this->img = imagecreatefromjpeg($filename);
                break;
            case 'png' :
                $this->img = imagecreatefrompng($filename);
                break;
            case 'gif' :
                $this->img = imagecreatefromgif($filename);
                break;
            default :
                return false; //failure on invalid extension
        }
        imagealphablending($this->img, true);
        imagesavealpha($this->img, true);
        return true; //if we are at this point, then everything went fine
    }

The above code does 4 basic jobs:

  • Checks to see if the file exists or not.
  • Extracts the extension of the file and validates it.
  • Creates the image object using GD library functions.
  • Sets the transparency channels and alpha blending mode.

By executing this method, we can load any image from a file and save it under the img public variable. Any operation shall be done with this variable only. Also, it stores the type of the image inside the type variable.

#2.3: The save_image method:

    /**
     * Saves the image in a file
     *
     * The image from img variable is saved
     *
     * @param string $filename The path where the file is to be saved
     * @param string $type The type of the saved file. Use inherit for original, or use jpg|jpeg|png|gif
     * @param int $jpeg_quality The quality of the jpeg image (used only if being saved as jpeg), 0 - 100 (max quality)
     * @param int $png_compression The compression of the PNG image (used only if being saved as png), 0 (max quality, min compression) - 9
     * @return boolean true on success, false otherwise
     */
    public function save_image($filename, $type = 'inherit', $jpeg_quality = 100, $png_compression = 0) {
        $ext = 'inherit' == $type ? $this->type : $type;
        switch($ext) {
            case 'jpg' :
            case 'jpeg' :
                imagejpeg($this->img, $filename, $jpeg_quality);
                break;
            case 'png' :
                imagepng($this->img, $filename, $png_compression = 0);
                break;
            case 'gif' :
                imagegif($this->img, $filename);
                break;
            default :
                return false;
        }
        return true;
    }

As you can see, it accepts three parameters and saves the image in a file.

  • $filename: The path (relative or absolute) of the file where we need to save the image. This will simply be created.
  • $type: We can specify a specific type (jpg, png or gif) if we want to convert the image from one to another. Leave it inherit to simply save according to the parents format.
  • $jpeg_quality: Is interpreted only if we are saving a jpeg image. The quality can be 0 to 100.
  • $png_compression: The compression value of the PNG. 0 means no compression and maximum quality, whereas 9 is the maximum compression.

Recommended Reading: imagejpeg, imagepng, imagegif.

#2.4: The output method:

    /**
     * Output the image directly into the browser.
     * Also stops execution of other scripts by calling die() function
     *
     * @uses die
     * @param string $type The type of the saved file. Use inherit for original, or use jpg|jpeg|png|gif
     * @param int $jpeg_quality The quality of the jpeg image (used only if being saved as jpeg), 0 - 100 (max quality)
     * @param int $png_compression The compression of the PNG image (used only if being saved as png), 0 (max quality, min compression) - 9
     * @return boolean false on failure
     */
    public function output($type = 'inherit', $jpeg_quality = 100, $png_compression = 0) {
        if($this->img == false)
            return false;

        header('Content-type: image/' . $this->type);
        $this->save_image(null, $type, $jpeg_quality, $png_compression);
        //die to stop execution
        die();
    }

This method simply ouputs the image directly into the browser by setting the proper header and calling the save_image method itself. Note that we have passed null in place of $filename so that it output to the browser itself (ie, the stdout).

#2.5: The flip_image method:

    /**
     * Flip the image horizontally, vertically or both
     *
     * Manipulates the image object saved in the img variable
     *
     * @param string $type Flipping type, can be hori|horizontal|vert|vertical|both
     * @return boolean true on success, false on failure
     */
    public function flip_image($type) {
        //little bit of error check
        if(false === $this->img) {
            return false;
        }
        //first get the height and width
        $width = imagesx($this->img);
        $height = imagesy($this->img);

        //create the empty destination image
        $dest = imagecreatetruecolor($width, $height);
        imagealphablending($dest, false);
        imagesavealpha($dest, true);

        //now work with the type and do the necessary flipping
        switch($type) {
            case 'vert' : //vertical flip
            case 'vertical' :
                for($i = 0; $i < $height; $i++) {
                    /**
                     * What we do here is pixel wise row flipping
                     * The first row of pixels of the source image (ie, when $i = 0)
                     * goes to the last row of pixels of the destination image
                     *
                     * So, mathematically, for the row $i of the source image
                     * the corresponding row of the destination should be
                     * $height - $i - 1
                     * -1, because y and x both co-ordinates are calculated from zero
                     */
                    imagecopy($dest, $this->img, 0, ($height - $i - 1), 0, $i, $width, 1);
                }
                break;

            case 'hori' : //horizontal flip
            case 'horizontal' :
                for($i = 0; $i < $width; $i++) {
                    /**
                     * Here we apply the same logic for other direction
                     * The first column of pixels of the source image
                     * goes to the last column of pixels of the destination image
                     *
                     * So, for the $i -th column of the source
                     * the column of the destination would be
                     * $width - $i - 1
                     */
                    imagecopy($dest, $this->img, ($width - $i - 1), 0, $i, 0, 1, $height);
                }
                break;

            case 'both' :
                //we simply return using recursive call
                if($this->flip_image('horizontal') && $this->flip_image('vertical'))
                    return true;
                else
                    return false;
                break;
            default :
                return false;
        }

        //now make the changes
        imagedestroy($this->img);
        $this->img = $dest;
        return true;
    }

So this is one of the real methods. We have already discussed the methodology. It accepts only one argument $type, which can be vertical, horizontal or both for corresponding flipping.

#2.6: The rotate_image method:

    /**
     * Rotate the image to the given angle, filled with given color and alpha
     *
     * Rotates in clockwise direction and takes hexadecimal color code as input
     *
     * @param int $angle The rotational angle
     * @param string $bgd_color The background color code in hex. Optional, default is ffffff (white)
     * @param int $alpha The alpha value, 0 for opaque, 127 for transparent, anything between for translucent
     * @return void
     */
    public function rotate_image($angle, $bgd_color = 'ffffff', $alpha = 0) {
        if($angle == 0)
            return;
        $angle = abs($angle);
        //make the value for clockwise rotation
        $r_angle = 360 - ($angle % 360);

        extract($this->hex_to_rgb($bgd_color));
        $color = imagecolorallocatealpha($this->img, $red, $green, $blue, $alpha);

        $dest = imagerotate($this->img, $r_angle, $color);

        if(false !== $dest) {
            imagealphablending($dest, true);
            imagesavealpha($dest, true);
            imagedestroy($this->img);
            $this->img = $dest;
        }

    }

As we can see,

  • First we calculate the angle value for proper clockwise rotation.
  • Then we convert the hexadecimal color to rgb and create a color code using imagecolorallocatealpha library function. Here we also take the alpha transparency value.
  • We simply rotate the image and save it under the img variable.

It accepts three parameters:

  • $angle : The angle value in degree by which the image is to be rotated clockwise.
  • $bgd_color : The hexadecimal code of the background color.
  • $alpha : The alpha value of the background. 0 for complete opaque and 127 for transparent. Anything in between will make it translucent.

#3: Code analysis:

Let us analyze a few part of the source code and the concept behind them.

#3.1: Transparency:

Transparency of PNG images is retained using imagealphablending and imagesavealpha GD functions.

The first one is here just for completeness. It enables blending alpha values between several layers. It becomes handy when we merge two images with transparency. But, here we are just flipping and rotating the images, so it was not needed. But, we have added it here, so that we understand concepts better in our future tutorials.

imagesavealpha is the function which enables us to save the alpha information inside an image object. It is set false by default. So we have enabled it.

        imagealphablending($this->img, true);
        imagesavealpha($this->img, true);

Recommended Reading: imagealphablending, imagesavealpha.

#3.2: Creating/Destroying image objects:

Whenever we have done any operation on the image, we have first created an destination image object using imagecreatetruecolor. It, as the name suggests, creates a true color image where the transparency operation can be done.

        //first get the height and width
        $width = imagesx($this->img);
        $height = imagesy($this->img);

        //create the empty destination image
        $dest = imagecreatetruecolor($width, $height);
        imagealphablending($dest, false);
        imagesavealpha($dest, true);

In case of rotation though, we did not have to create an object, because the function itself creates one and returns it.

When we are done with an object, in order to save memory, we always destroy that image object. For that, we have used imagedestroy function.

        //now make the changes
        imagedestroy($this->img);
        $this->img = $dest;

Recommended Reading: imagecreatetruecolor, imagedestroy.

#3.3: Generating the color:

As said earlier, color code generation is not as simple as passing hex values. So, we have used a private method to convert hex values into rgb:

    /**
     * Converts hexadecimal to RGB color array
     * @link http://www.anyexample.com/programming/php/php_convert_rgb_from_to_html_hex_color.xml
     * @uses hexdec http://php.net/manual/en/function.hexdec.php
     * @access private
     * @param string $color 6 or 3 character long hexadecimal code
     * @return array with red, green, blue keys and corresponding values
     */
    private function hex_to_rgb($color) {
        if($color[0] == '#')
            $color = substr($color, 1);

        if(strlen($color) == 6) {
            list($r, $g, $b) = array(
                $color[0].$color[1],
                $color[2].$color[3],
                $color[4].$color[5]
            );
        } elseif (strlen($color) == 3) {
            list($r, $g, $b) = array(
                $color[0].$color[0],
                $color[1].$color[1],
                $color[2].$color[2]
            );
        } else {
            return array('red' => 255, 'green' => 255, 'blue' => 255);
        }

        $r = hexdec($r); $g = hexdec($g); $b = hexdec($b);
        return array('red' => $r, 'green' => $g, 'blue' => $b);
    }

Then, we have used imagecolorallocatealpha to create the color code corresponding to that rgb and alpha value.

        extract($this->hex_to_rgb($bgd_color));
        $color = imagecolorallocatealpha($this->img, $red, $green, $blue, $alpha);

We could have also used imagecolorallocate function to generate color codes without alpha values.

Recommended Reading: imagecolorallocatealpha, imagecolorallocate

#3.4: Pixel-wise copying:

As seen in the flipping code, we do a pixel wise copy of the image. This is done using the imagecopy function. We split the source image into rows or columns of height/length 1px and copy it to the newer position of the destination image.

case 'vert' : //vertical flip
            case 'vertical' :
                for($i = 0; $i < $height; $i++) {
                    /**
                     * What we do here is pixel wise row flipping
                     * The first row of pixels of the source image (ie, when $i = 0)
                     * goes to the last row of pixels of the destination image
                     *
                     * So, mathematically, for the row $i of the source image
                     * the corresponding row of the destination should be
                     * $height - $i - 1
                     * -1, because y and x both co-ordinates are calculated from zero
                     */
                    imagecopy($dest, $this->img, 0, ($height - $i - 1), 0, $i, $width, 1);
                }
                break;

            case 'hori' : //horizontal flip
            case 'horizontal' :
                for($i = 0; $i < $width; $i++) {
                    /**
                     * Here we apply the same logic for other direction
                     * The first column of pixels of the source image
                     * goes to the last column of pixels of the destination image
                     *
                     * So, for the $i -th column of the source
                     * the column of the destination would be
                     * $width - $i - 1
                     */
                    imagecopy($dest, $this->img, ($width - $i - 1), 0, $i, 0, 1, $height);
                }
                break;

Recommended Reading: imagecopy

#4: Complete Code:

So, we put together the code and the result would be:

/**
 * The Image Manupulation class
 * Currently can only rotate and flip an image
 * More will be added soon
 * @author Swashata <swashata at intechgrity.com>
 * @license Free
 */
class ImageManipulation {

    /**
     * The image container variable
     * Should be false if no image present
     * Otherwise should hold the GD Image Object
     * @var &gd.return.identifier
     */
    public $img;

    /**
     * The type of the image loaded
     * Should be false or any of the jpg|jpeg|png|gif
     * @var string
     */
    public $type;

    /**
     * The constructor function
     *
     * Set the default false value to both the variable
     */
    public function __construct() {
        $this->img = false;
        $this->type = false;
    }

    /**
     * The loader function
     * Loads the image from a file to the $this->img variable
     *
     * Also checks if the file exists and of valid image type by checking the extension
     * @param string $filename The path of the file to be loaded
     * @return boolean true on success, false otherwise
     */
    public function load($filename) {
        //first check if the file exists
        if(!file_exists($filename))
            return false;

        //extract the extension & thereby check if valid image
        $ext = '';
        //we check the extension, it can be 4 characters (for jpeg) and must be atleast 3 characters
        if(preg_match('/.*\.([A-Za-z]?[A-Za-z]{3})$/', $filename, $ext)) {
            //var_dump($ext);
            $ext = $ext[1];
            //var_dump($ext);
        } else {
            //invalid image
            return false;
        }

        $this->type = $ext;
        switch($ext) {
            case 'jpg' :
            case 'jpeg' :
                $this->img = imagecreatefromjpeg($filename);
                break;
            case 'png' :
                $this->img = imagecreatefrompng($filename);
                break;
            case 'gif' :
                $this->img = imagecreatefromgif($filename);
                break;
            default :
                return false; //failure on invalid extension
        }
        imagealphablending($this->img, true);
        imagesavealpha($this->img, true);
        return true; //if we are at this point, then everything went fine
    }

    /**
     * Saves the image in a file
     *
     * The image from img variable is saved
     *
     * @param string $filename The path where the file is to be saved
     * @param string $type The type of the saved file. Use inherit for original, or use jpg|jpeg|png|gif
     * @param int $jpeg_quality The quality of the jpeg image (used only if being saved as jpeg), 0 - 100 (max quality)
     * @param int $png_compression The compression of the PNG image (used only if being saved as png), 0 (max quality, min compression) - 9
     * @return boolean true on success, false otherwise
     */
    public function save_image($filename, $type = 'inherit', $jpeg_quality = 100, $png_compression = 0) {
        $ext = 'inherit' == $type ? $this->type : $type;
        switch($ext) {
            case 'jpg' :
            case 'jpeg' :
                imagejpeg($this->img, $filename, $jpeg_quality);
                break;
            case 'png' :
                imagepng($this->img, $filename, $png_compression = 0);
                break;
            case 'gif' :
                imagegif($this->img, $filename);
                break;
            default :
                return false;
        }
        return true;
    }

    /**
     * Output the image directly into the browser.
     * Also stops execution of other scripts by calling die() function
     *
     * @uses die
     * @param string $type The type of the saved file. Use inherit for original, or use jpg|jpeg|png|gif
     * @param int $jpeg_quality The quality of the jpeg image (used only if being saved as jpeg), 0 - 100 (max quality)
     * @param int $png_compression The compression of the PNG image (used only if being saved as png), 0 (max quality, min compression) - 9
     * @return boolean false on failure
     */
    public function output($type = 'inherit', $jpeg_quality = 100, $png_compression = 0) {
        if($this->img == false)
            return false;

        header('Content-type: image/' . $this->type);
        $this->save_image(null, $type, $jpeg_quality, $png_compression);
        //die to stop execution
        die();
    }

    /**
     * Flip the image horizontally, vertically or both
     *
     * Manipulates the image object saved in the img variable
     *
     * @param string $type Flipping type, can be hori|horizontal|vert|vertical|both
     * @return boolean true on success, false on failure
     */
    public function flip_image($type) {
        //little bit of error check
        if(false === $this->img) {
            return false;
        }
        //first get the height and width
        $width = imagesx($this->img);
        $height = imagesy($this->img);

        //create the empty destination image
        $dest = imagecreatetruecolor($width, $height);
        imagealphablending($dest, false);
        imagesavealpha($dest, true);

        //now work with the type and do the necessary flipping
        switch($type) {
            case 'vert' : //vertical flip
            case 'vertical' :
                for($i = 0; $i < $height; $i++) {
                    /**
                     * What we do here is pixel wise row flipping
                     * The first row of pixels of the source image (ie, when $i = 0)
                     * goes to the last row of pixels of the destination image
                     *
                     * So, mathematically, for the row $i of the source image
                     * the corresponding row of the destination should be
                     * $height - $i - 1
                     * -1, because y and x both co-ordinates are calculated from zero
                     */
                    imagecopy($dest, $this->img, 0, ($height - $i - 1), 0, $i, $width, 1);
                }
                break;

            case 'hori' : //horizontal flip
            case 'horizontal' :
                for($i = 0; $i < $width; $i++) {
                    /**
                     * Here we apply the same logic for other direction
                     * The first column of pixels of the source image
                     * goes to the last column of pixels of the destination image
                     *
                     * So, for the $i -th column of the source
                     * the column of the destination would be
                     * $width - $i - 1
                     */
                    imagecopy($dest, $this->img, ($width - $i - 1), 0, $i, 0, 1, $height);
                }
                break;

            case 'both' :
                //we simply return using recursive call
                if($this->flip_image('horizontal') && $this->flip_image('vertical'))
                    return true;
                else
                    return false;
                break;
            default :
                return false;
        }

        //now make the changes
        imagedestroy($this->img);
        $this->img = $dest;
        return true;
    }

    /**
     * Rotate the image to the given angle, filled with given color and alpha
     *
     * Rotates in clockwise direction and takes hexadecimal color code as input
     *
     * @param int $angle The rotational angle
     * @param string $bgd_color The background color code in hex. Optional, default is ffffff (white)
     * @param int $alpha The alpha value, 0 for opaque, 127 for transparent, anything between for translucent
     * @return void
     */
    public function rotate_image($angle, $bgd_color = 'ffffff', $alpha = 0) {
        if($angle == 0)
            return;
        $angle = abs($angle);
        //make the value for clockwise rotation
        $r_angle = 360 - ($angle % 360);

        extract($this->hex_to_rgb($bgd_color));
        $color = imagecolorallocatealpha($this->img, $red, $green, $blue, $alpha);

        $dest = imagerotate($this->img, $r_angle, $color);

        if(false !== $dest) {
            imagealphablending($dest, true);
            imagesavealpha($dest, true);
            imagedestroy($this->img);
            $this->img = $dest;
        }

    }

    /**
     * Converts hexadecimal to RGB color array
     * @link http://www.anyexample.com/programming/php/php_convert_rgb_from_to_html_hex_color.xml
     * @uses hexdec http://php.net/manual/en/function.hexdec.php
     * @access private
     * @param string $color 6 or 3 character long hexadecimal code
     * @return array with red, green, blue keys and corresponding values
     */
    private function hex_to_rgb($color) {
        if($color[0] == '#')
            $color = substr($color, 1);

        if(strlen($color) == 6) {
            list($r, $g, $b) = array(
                $color[0].$color[1],
                $color[2].$color[3],
                $color[4].$color[5]
            );
        } elseif (strlen($color) == 3) {
            list($r, $g, $b) = array(
                $color[0].$color[0],
                $color[1].$color[1],
                $color[2].$color[2]
            );
        } else {
            return array('red' => 255, 'green' => 255, 'blue' => 255);
        }

        $r = hexdec($r); $g = hexdec($g); $b = hexdec($b);
        return array('red' => $r, 'green' => $g, 'blue' => $b);
    }
}

#5: Usage of the Code:

Usage of the code is pretty simple and straight forward.

First we load our image file:

//load the image
$img = new ImageManipulation();
$img->load('linux.' . $type);

Then we do the rotation and flipping:

$img->rotate_image($ro, 'ffffff', 127);
$img->flip_image($fl);

Then we simply save or output the image:

$img->output();
$img->save_image('newfile.jpg', 'jpg');

Easy enough? Check the download package and execute it right on your localhost to get the right idea.

Conclusion:

So that was all about image rotation and flipping in PHP using GD library functions. The main advantages are:

  • Lightweight and easy to understand source code.
  • PHP-GD is preinstalled in most of the modern days web servers.
  • OOP type code for perfect re-usability.

In the next tutorial of this series, we shall see how to crop images with proper proportion along with resize. So stay tuned. And, if you have any doubt, or have anything to say, please do so in the comments.

Credit: Linux Tux icon used in the demo is from IconShock

Leave a Reply