Programming and Technology
RSS icon Home icon
  • Object Oriented GD

    Posted on April 6th, 2009 brandon 1 comment

    Sooner or later we all need to use the gd library whether it be for generating thumbnails, scaling images, cropping, or whatever. When compiled with gd support, PHP offers a massive arsenal of functions to accomplish all of the image related tasks you can think of. With the advent of object oriented frameworks such as the Zend Framework and others, it can get a little awkward mixing procedural code with with objects. Additionally code for processing images can get quite ugly with all of the variable juggling.

    After a number of image heavy projects, I got tired of the cut’n'paste headache and decided that it would be much easier to maintain graphical applications that interacted with images in an object-oriented fashion. I developed a small tool chain for encapsulating images and mechanisms for processing them. I start with a simple class definition which allows me to treat an image as an object.

    1.  
    2. <?php
    3.  
    4. class FP_Image
    5. {
    6.   const PNG = ‘png’;
    7.   const JPEG = ‘jpg’;
    8.   const GIF = ‘gif’;
    9.  
    10.   protected $image = null;
    11.   protected $width = null;
    12.   protected $height = null;
    13.  
    14.   public function __construct($pFile, $pMimetype=null){
    15.     $this->image = $this->create($pFile, $pMimetype);
    16.     $this->width = imageSX($this->image);
    17.     $this->height= imageSY($this->image);
    18.   }
    19.  
    20.   protected function create($pFile, $pMimetype=null){        
    21.     if (preg_match(‘/^\w+:\/\/.*/’, $pFile)){                 //Image path is url
    22.       $meta = stream_get_meta_data(fopen($pFile, ‘r’));
    23.      
    24.       foreach ($meta[‘wrapper_data’] as $header){
    25.         if (preg_match(‘/^Location:\ /’, $header)){            //Is this a redirect? Get real url
    26.           $redirect = true;
    27.           $url = preg_replace(‘/^Location:\ /’, , $header);
    28.         }
    29.       }
    30.  
    31.       if (!$redirect)
    32.         $url = $pFile;
    33.  
    34.       $url_path = parse_url($url, PHP_URL_PATH);
    35.       $path_parts = pathinfo($url_path);
    36.       $ext = $path_parts[‘extension’];
    37.     } else {                                                   //Just a file
    38.       $path_parts = pathinfo($pFile);
    39.       $ext = $path_parts[‘extension’];
    40.       $url = $pFile;
    41.     }
    42.  
    43.     if ($pMimetype){
    44.       logdebug(‘FP_Image.create’, "Forcing mimetype $pMimetype");
    45.       switch ($pMimetype){
    46.       case ‘image/jpeg’:
    47.         $res = imagecreatefromjpeg($url);
    48.         break;
    49.       case ‘image/png’:
    50.         $res = imagecreatefrompng($url);
    51.         break;
    52.       case ‘image/gif’:
    53.         $res = imagecreatefromgif($url);
    54.         break;
    55.       default:
    56.         throw new FP_Image_Exception("Unsupported format $url as $pMimetype", FP_Image_Exception::UNSUPPORTED_FORMAT);
    57.       }
    58.     } else {
    59.       switch(strtolower($ext)){
    60.       case ‘jpg’:
    61.       case ‘jpeg’:
    62.         $res = imagecreatefromjpeg($url);  
    63.         break;
    64.       case ‘png’:
    65.         $res = imagecreatefrompng($url);
    66.         break;
    67.       case ‘gif’:
    68.         $res = imagecreatefromgif($url);
    69.         break;
    70.       default:
    71.         throw new FP_Image_Exception("Unsupported format $url", FP_Image_Exception::UNSUPPORTED_FORMAT);
    72.       }
    73.     }
    74.     if (!is_resource($res))
    75.       throw new FP_Image_Exception("An error occured while loading $url. Please verify that the image exists and is not corrupt", FP_Image_Exception::LOAD_ERROR);
    76.    
    77.     return $res;
    78.   }
    79.  
    80.   public function __get($pAtt){
    81.     switch (strtolower($pAtt)){
    82.     case ‘width’:
    83.       return $this->width;
    84.     case ‘height’:
    85.       return $this->height;
    86.     case ‘image’:
    87.       return $this->image;
    88.     }
    89.   }
    90.  
    91.   public function save($pDestination){
    92.     $path_parts = pathinfo($pDestination);
    93.     $ext = $path_parts[‘extension’];
    94.  
    95.     switch (strtolower($ext)){
    96.     case ‘jpg’:
    97.     case ‘jpeg’:
    98.       return imagejpeg($this->image,$pDestination, 100);    
    99.     case ‘png’:
    100.       return imagepng($this->image,$pDestination, 9);      
    101.     case ‘gif’:
    102.       return imagegif($this->image,$pDestination);
    103.     default:
    104.       return false;
    105.     }
    106.   }
    107.  
    108.   public function stream($pFormat=self::PNG){  
    109.     switch ($pFormat){
    110.     case self::PNG:
    111.       return imagepng($this->image);      
    112.     case self::JPEG:
    113.       return imagejpeg($this->image);        
    114.     case self::GIF:
    115.       return imagegif($this->image);    
    116.     default:
    117.       throw new FP_Image_Exception("Unsupported format $pFormat", FP_Image_Exception::UNSUPPORTED_FORMAT);      
    118.     }
    119.   }
    120.  
    121.   public function __destroy(){
    122.     imagedestroy($this->image);
    123.   }
    124. }
    125. ?>
    126.  

    This class takes a file path or url as its constructor argument. Using some tricky http header sniffing we are able to determine if the provided url is actually a redirect and in turn we are able to get the real url of the image. This is a handy feature as it allows us to process remotely hosted images on the fly which gives us the option of using CDNs or clouds for image storage. Next a gd resource is created and stored internally.

    The object exposes 3 properties (width, height, and image). The first two are obvious while the image property is a reference to the gd resource should we ever need to deal with it directly. The save method allows the image to be saved to a file in the format specified by the extension of the destination path. The stream function is useful for streaming the raw image data to a web browser when you need to link to a dynamically generated image. The object destructor frees the gd resource from memory.

    So at first glance this class isn’t terribly useful aside from the fact that it handles the image format automatically. It’s true utility comes by extending it to encapsulate additional functionality. We can use this class as a rich base class that provides the mundane common functionality that all of the subclasses need. The simplest example would be probably the most common use of gd on the web: the generation of thumbnails

    Generate Thumbnails

    1.  
    2. <?php
    3.  
    4. class FP_Image_Thumbnail extends FP_Image
    5. {
    6.  
    7.   public function __construct($pFile, $pWidth, $pHeight, $pDestination=null, $pMimetype=null){
    8.     if (is_resource($pFile))
    9.       $img = $pFile;
    10.     else if (is_object($pFile) && $pFile instanceof FP_Image)
    11.       $img = $pFile->image;
    12.     else
    13.       $img = $this->create($pFile, $pMimetype);
    14.  
    15.     $owidth = imageSX($img);
    16.     $oheight= imageSY($img);
    17.  
    18.     if ($pWidth < $owidth)
    19.       $nwidth = floor(($pWidth/$owidth) * $owidth);
    20.     else
    21.       $nwidth = $owidth;
    22.  
    23.     if ($pHeight < $oheight)
    24.       $nheight = floor($pHeight/$oheight * $oheight);
    25.     else
    26.       $nheight = $oheight;
    27.  
    28.     $this->image = ImageCreateTrueColor($nwidth, $nheight);
    29.     imagecopyresampled($this->image, $img, 0, 0, 0, 0, $nwidth, $nheight, $owidth, $oheight);
    30.  
    31.     imagedestroy($img);
    32.  
    33.     if ($pDestination)
    34.       $this->save($pDestination);
    35.   }
    36. }
    37. ?>
    38.  

    Here all we overload is the constructor. This class has the added bonus of being able to accept other objects that extend the FP_Image class as well as raw gd resources. The constructor takes this agnostic image as well as a width and height desired for the thumbnail. The class does all of the processing it needs and then stores the created thumbnail internally which we can then access using the methods provided by FP_Image.

    Here is a quick example of how to generate dynamic thumbnails from a Zend Framework controller action using this class:

    1.  
    2. <?php
    3. class ImageController extend Zend_Controller_Action {
    4.  
    5.   public function thumbnailAction(){
    6.     $this->_response->setHeader(‘Content-Type’, ‘image/png’, true)
    7.                             ->setHeader(‘Content-Transfer-Encoding’, ‘binary’, true);
    8.  
    9.     $thumbnail = new FP_Image_Thumbnail(‘/var/www/test/images/avatar.jpg’, 100, 100);    
    10.     $thumbnail->stream();
    11.   }
    12.  
    13. }
    14. ?>
    15.  

    Scale Images

    1.  
    2. <?php
    3.  
    4. class FP_Image_Scale extends FP_Image
    5. {
    6.   const HORIZONTAL = ‘horizontal’;
    7.   const VERTICAL = ‘vertical’;
    8.  
    9.   public function __construct($pFile, $pMaxSize, $pScaleMode=self::HORIZONTAL, $pDestination=null, $pMimetype=null){
    10.     logdebug(‘FP_Image_Scale.__construct’, "Scaling ". logobject($pFile) ." to $pScaleMode of $pMaxSize");
    11.  
    12.     if (is_resource($pFile))
    13.       $img = $pFile;
    14.     else if (is_object($pFile) && $pFile instanceof FP_Image)
    15.       $img = $pFile->image;
    16.     else
    17.       $img = $this->create($pFile, $pMimetype);
    18.  
    19.     $owidth = imageSX($img);
    20.     $oheight= imageSY($img);
    21.  
    22.     switch ($pScaleMode){
    23.     case self::HORIZONTAL:      
    24.       $width = $pMaxSize;
    25.       $height = round(($oheight * $width) / $owidth);
    26.       break;
    27.     case self::VERTICAL:
    28.       $height = $pMaxSize;
    29.       $width = round(($owidth * $height) / $oheight);
    30.     default:
    31.       throw new Exception(‘Unhandled scale mode ‘.$pScaleMode);
    32.     }
    33.  
    34.     $this->image = ImageCreateTrueColor($width, $height);
    35.     imagecopyresampled($this->image, $img, 0, 0, 0, 0, $width, $height, $owidth, $oheight);
    36.    
    37.     $this->width = $width;
    38.     $this->height = $height;
    39.  
    40.     imagedestroy($img);
    41.  
    42.     if ($pDestination)
    43.       $this->save($pDestination);
    44.   }
    45. }
    46. ?>
    47.  

    This class isn’t all that different, instead of scaling down both dimensions it scales either vertically or horizontally while preserving the aspect ratio.

    Crop Images

    1.  
    2. <?php
    3.  
    4. class FP_Image_Crop extends FP_Image
    5. {
    6.   public function __construct($pFile, $pX, $pY, $pWidth, $pHeight, $pDestination=null){
    7.     //logdebug(‘FP_Image_Crop.__construct’, "Cropping $pFile at $pX,$pY ($pWidth x $pHeight) destination=$pDestination");
    8.  
    9.     if (is_resource($pFile))
    10.       $original = $pFile;
    11.     else if (is_object($pFile) && $pFile instanceof FP_Image)
    12.       $original = $pFile->image;
    13.     else
    14.       $original = $this->create($pFile);
    15.  
    16.     $this->image = imagecreatetruecolor($pWidth, $pHeight);
    17.     imagecopyresampled($this->image, $original, 0, 0, $pX, $pY, $pWidth, $pHeight, $pWidth, $pHeight);  
    18.  
    19.     imagedestroy($original);
    20.  
    21.     if ($pDestination){
    22.       $this->save($pDestination);
    23.     }
    24.   }
    25. }
    26. ?>
    27.  

    This class takes another image and crops it to the specifications passed into the constructor.

    Watermark an Image

    1.  
    2. <?php
    3.  
    4.   //Overlay image ontop of another
    5. class FP_Image_Watermark extends FP_Image
    6. {
    7.   public function __construct($pBaseFile, $pOverlayFile, $pDX, $pDY, $pSX, $pSY, $pSWidth, $pSHeight, $pAlphaBlending=true, $pDestination=null, $pMimetype=null){
    8.     if (is_resource($pBaseFile))
    9.       $base = $pBaseFile;
    10.     else if (is_object($pBaseFile) && $pBaseFile instanceof FP_Image)
    11.       $base = $pBaseFile->image;
    12.     else
    13.       $base = $this->create($pBaseFile, $pMimetype);
    14.  
    15.     if (is_resource($pOverlayFile))
    16.       $overlay = $pOverlayFile;
    17.     else if (is_object($pOverlayFile) && $pOverlayFile instanceof FP_Image)
    18.       $overlay = $pOverlayFile->image;
    19.     else
    20.       $overlay = $this->create($pOverlayFile);
    21.  
    22.     imagealphablending($overlay, $pAlphaBlending);
    23.  
    24.     imagecopy($base, $overlay, $pDX, $pDY, $pSX, $pSY, $pSWidth, $pSHeight);
    25.     $this->image = $base;
    26.  
    27.     $this->width = imageSX($this->image);
    28.     $this->height = imageSY($this->image);
    29.  
    30.  
    31.     if ($pDestination)
    32.       $this->save($pDestination);
    33.   }
    34. }
    35. ?>
    36.  

    This is a handy class for embedding a watermark into an image. This takes a base image and a secondary image and overlays it with alpha blending allowing for subtle semi-transparent watermarks.

    Overlay True Type Text over Image

    1.  
    2. <?php
    3.  
    4. class FP_Image_MultiLineOverlay extends FP_Image {
    5.  
    6.   const LEFT = ‘left’;
    7.   const CENTER = ‘center’;
    8.   const RIGHT = ‘right’;
    9.  
    10.   protected $text = null;
    11.   protected $angle = null;
    12.   protected $font_url = null;
    13.   protected $font_size = 10;
    14.   protected $leading = 0;
    15.   protected $justify = self::LEFT;
    16.   protected $foreground_color = array(255,255,255);
    17.   protected $text_x = 0;
    18.   protected $text_y = 0;
    19.   protected $padding = array(‘t’ => 0, ‘r’ => 0, ‘b’ => 0, ‘l’ => 0);
    20.   protected $truncate_at = 0;
    21.  
    22.   public function __construct($pBaseFile, $pText, $pFontUrl, array $pOptions=array(), $pDestination=null, $pMimetype=null){
    23.     if (is_resource($pBaseFile))
    24.       $this->image = $pBaseFile;
    25.     else if (is_object($pBaseFile) && $pBaseFile instanceof FP_Image)
    26.       $this->image = $pBaseFile->image;
    27.     else
    28.       $this->image = $this->create($pBaseFile, $pMimetype);
    29.  
    30.     $this->width = imageSX($this->image);
    31.     $this->height= imageSY($this->image);
    32.  
    33.     $this->text = $pText;
    34.     $this->font_url = $pFontUrl;
    35.  
    36.     if (array_key_exists(‘font_size’, $pOptions))
    37.       $this->font_size = $pOptions[‘font_size’];
    38.  
    39.     if (array_key_exists(‘justify’, $pOptions))
    40.       $this->justify = $pOptions[‘justify’];
    41.  
    42.     if (array_key_exists(‘leading’, $pOptions))
    43.       $this->leading = $pOptions[‘leading’];
    44.  
    45.     if (array_key_exists(‘font_size’, $pOptions))
    46.       $this->font_size = $pOptions[‘font_size’];
    47.  
    48.     if (array_key_exists(‘foreground_color’, $pOptions))
    49.       $this->foreground_color = $pOptions[‘foreground_color’];
    50.  
    51.     if (array_key_exists(‘truncate’, $pOptions))
    52.       $this->truncate_at = $pOptions[‘truncate’];
    53.  
    54.  
    55.     if (array_key_exists(‘padding’, $pOptions)){
    56.       $padding = $pOptions[‘padding’];
    57.       if (array_key_exists(‘t’, $padding))
    58.         $this->padding[‘t’] = $padding[‘t’];
    59.       if (array_key_exists(‘r’, $padding))
    60.         $this->padding[‘r’] = $padding[‘r’];
    61.       if (array_key_exists(‘b’, $padding))
    62.         $this->padding[‘b’] = $padding[‘b’];
    63.       if (array_key_exists(‘l’, $padding))
    64.         $this->padding[‘l’] = $padding[‘l’];      
    65.     }
    66.  
    67.     if (array_key_exists(‘angle’, $pOptions))
    68.       $this->angle = $pOptions[‘angle’];
    69.  
    70.     $test_text = ‘abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_’;
    71.     $_globalbox = imageTTFBbox($this->font_size, 0, $this->font_url, $test_text);
    72.     $globalbox = convertBoundingBox($_globalbox);
    73.  
    74.     $width = $this->width;
    75.     $height = $this->height;
    76.  
    77.     $text_color = imagecolorallocate($this->image, $this->foreground_color[0],$this->foreground_color[1],$this->foreground_color[2]);
    78.  
    79.     if ($this->justify == self::LEFT){
    80.       if ($this->truncate_at > 0)
    81.         $text = truncate($this->text, $this->truncate_at);
    82.       else
    83.         $text = $this->text;
    84.  
    85.       $text_x = $this->text_x + $this->padding[‘l’];
    86.       imagettftext($this->image, $this->font_size, $this->angle, $text_x, $this->font_size, $text_color, $this->font_url, $text);
    87.     } else {
    88.       $strings = preg_split(‘/\n/’, $this->text);
    89.       $__H = $this->padding[‘t’];
    90.  
    91.       if ($this->justify == self::RIGHT) {
    92.  
    93.         foreach ($strings as $key => $val){
    94.           if ($this->truncate_at > 0)
    95.             $text = truncate($val, $this->truncate_at);
    96.           else
    97.             $text = $val;
    98.          
    99.           $_b = imageTTFBbox($this->font_size, 0, $this->font_url, $text);
    100.           $box = convertBoundingBox($_b);
    101.  
    102.           $_W = $box[‘width’];
    103.  
    104.           $_X = $width$_W$this->padding[‘r’];
    105.          
    106.           $_H = $globalbox[‘height’];
    107.           $__H += $_H + $this->leading;     
    108.  
    109.           imagettftext($this->image, $this->font_size, $this->angle, $_X, $__H, $text_color, $this->font_url, $text);
    110.         }
    111.       } else if ($this->justify == self::CENTER){
    112.  
    113.         foreach ($strings as $key => $val){
    114.           if ($this->truncate_at > 0)
    115.             $text = truncate($val, $this->truncate_at);
    116.           else
    117.             $text = $val;
    118.  
    119.           $_b = imageTTFBbox($this->font_size, 0, $this->font_url, $text);
    120.           $box = convertBoundingBox($_b);
    121.          
    122.           $_W = $box[‘width’];
    123.  
    124.           $_X = abs($width / 2)abs($_W / 2);
    125.  
    126.           $_H = $globalbox[‘height’];
    127.  
    128.           $__H += $_H;
    129.           imagettftext($this->image, $this->font_size, $this->angle, $_X, $__H, $text_color, $this->font_url, $text);
    130.           $__H += $this->leading;
    131.         }
    132.       }
    133.     }
    134.  
    135.     if ($pDestination)
    136.       $this->save($pDestination);
    137.   }
    138. }
    139. ?>
    140.  

    This one is fairly sophisticated compared to the others. In a nutshell it allows multiple lines of true type text to be overlayed ontop of an image. It takes a wide array of configuration option to specify things like font color, padding, justification, angle, etc. This class probably deserves a post all in itself so trust me when I say it excels in utility.

    Because all of these classes all extend the same base class they can be used in elaborate combinations by overlaying text over a watermarked image that is then in turn scaled down. Also in this structure, these classes are completely generalized and are portable to any application that needs image processing.

    • Share/Bookmark
     

    One response to “Object Oriented GD”

    1. Patrick Barroca

      Thanks and congratulations for this OOP effort in image processing.
      I think all this classes are too resources hungry in php scripting context.
      I mean, I have a static class that does almost all the job yours do but it’s behavior is driven by configuration without the need of instantiating objects.
      Imagine the number of objects created when you’ll have for example a gallery to render with same images in different sizes, crops, watermarks, etc…

      By the way, your idea of passing raw GD resources is very cool and the call sequences it permits rocks.
      This will greatly simplify our configuration arrays ;)

    Leave a reply

Get Adobe Flash playerPlugin by wpburn.com wordpress themes