View file application/libraries/Engine/Image/Adapter/Gd.php

File size: 28.64Kb
<?php
/**
 * SocialEngine
 *
 * @category   Engine
 * @package    Engine_Image
 * @copyright  Copyright 2006-2010 Webligo Developments
 * @license    http://www.socialengine.com/license/
 * @version    $Id: Gd.php 9747 2012-07-26 02:08:08Z john $
 * @todo       documentation
 */

/**
 * @category   Engine
 * @package    Engine_Image
 * @copyright  Copyright 2006-2010 Webligo Developments 
 * @license    http://www.socialengine.com/license/
 */
class Engine_Image_Adapter_Gd extends Engine_Image
{
  /**
   * Image format support
   *
   * @var array
   */
  protected static $_support;
  protected $_file;
  protected $_format;
  protected $_height;
  protected $_width;
  protected $_fileContent;
  protected $_originalFramesMeta = array();
  protected $_meta = array();
  protected $_frames = array();

  // Methods

  /**
   * Constructor
   *
   * @param string $file Image to open
   */
  public function __construct($options = array())
  {
    // Check support
    if( !function_exists('gd_info') ) {
      throw new Engine_Image_Adapter_Exception('GD library is not installed');
    }
    parent::__construct($options);
  }

  // Options

  public function getFile()
  {
    return $this->_file;
  }

  public function setFile($file)
  {
    $this->_file = $file;
    return $this;
  }

  public function getFormat()
  {
    return $this->_format;
  }

  public function setFormat($format)
  {
    $format = strtolower($format);
    self::_isSupported($format);
    $this->_format = $format;
    return $this;
  }

  public function getHeight()
  {
    return $this->_height;
  }

  public function getWidth()
  {
    return $this->_width;
  }

  // Actions

  public function create($width, $height)
  {
    // Check if we can create the image
    self::_isSafeToOpen($width, $height);

    // Create image
    $resource = imagecreatetruecolor($width, $height);

    if( !$resource ) {
      throw new Engine_Image_Adapter_Exception("Unable to create image");
    }

    // Assign info
    $this->_width = $width;
    $this->_height = $height;
    $this->_resource = $resource;

    return $this;
  }

  /**
   * Open an image
   * 
   * @param string $file
   * @return Engine_Image_Adapter_Gd
   * @throws Engine_Image_Adapter_Exception If unable to open
   */
  public function open($file)
  {
    // Set file
    $this->_file = $file;

    // Get image info
    $info = @getimagesize($file);
    if( !$info ) {
      throw new Engine_Image_Adapter_Exception(sprintf("File \"%s\" is not an image or does not exist", $file));
    }

    // Check if we can open the file
    self::_isSafeToOpen($info[0], $info[1]);

    // Detect type
    $type = ltrim(strrchr('.', $file), '.');
    if( !$type ) {
      $type = self::image_type_to_extension($info[2], false);
    }
    $type = strtolower($type);

    // Set information
    $this->_format = $type;
    $this->_width = $info[0];
    $this->_height = $info[1];

    // Check support
    self::_isSupported($type);
    $function = 'imagecreatefrom' . $type;
    if( !function_exists($function) ) {
      throw new Engine_Image_Adapter_Exception(sprintf('Image type "%s" is not supported', $type));
    }

    if( $this->_isAnimatedGif() ) {
      $this->_openAnimated();
    } else {
      $this->_open();
    }
    return $this;
  }

  protected function _open()
  {
    $type = $this->_format;
    $file = $this->_file;
    // Open
    $function = 'imagecreatefrom' . $type;
    $this->_resource = $function($file);
    if( !$this->_checkOpenImage(false) ) {
      throw new Engine_Image_Adapter_Exception("Unable to open image");
    }
  }

  protected function _openAnimated()
  {
    $gif = new GIF_Decoder($this->_getFileContent());
    $this->_originalFramesMeta = $gif->GIFGetFramesMeta();
    if( count($this->_originalFramesMeta) <= 0 ) {
      return false;
    }
    $this->_meta = array(
      'delays' => $gif->GIFGetDelays(),
      'loops' => $gif->GIFGetLoop(),
      'disposal' => $gif->GIFGetDisposal(),
      'tr' => $gif->GIFGetTransparentR(),
      'tg' => $gif->GIFGetTransparentG(),
      'tb' => $gif->GIFGetTransparentB(),
      'trans' => (0 == $gif->GIFGetTransparentI() ? false : true)
    );
    $this->_frames = $gif->GIFGetFrames();
    return $this;
  }

  /**
   * Write current image to a file
   * 
   * @param string $file (OPTIONAL) The file to write to. Default: original file
   * @param string $type (OPTIONAL) The output image type. Default: jpeg
   * @return Engine_Image_Adapter_Gd
   * @throws Engine_Image_Adapter_Exception If unable to write
   */
  public function write($file = null)
  {
    // If no file specified, write to existing file
    if( null === $file ) {
      if( null === $this->_file ) {
        throw new Engine_Image_Adapter_Exception("No file to write specified.");
      }
      $file = $this->_file;
    }

    // Get output format
    $outputFormat = $this->_format;
    if( $this->_isAnimatedGif() ) {
      $this->_writeAnimated($file);
      return $this;
    }
    // Check support
    $function = 'image' . $outputFormat;
    if( !function_exists($function) ) {
      throw new Engine_Image_Adapter_Exception(sprintf('Image type "%s" is not supported', $outputFormat));
    }

    // Apply quality
    $quality = null;
    if( is_int($this->_quality) && $this->_quality >= 0 && $this->_quality <= 100 ) {
      $quality = $this->_quality;
    }

    // Write
    if( $function == 'imagejpeg' && null !== $quality ) {
      $result = $function($this->_resource, $file, $quality);
    } elseif( $function == 'imagepng' && null !== $quality ) {
      $result = $function($this->_resource, $file, round(abs(($quality - 100) / 11.111111)));
    } else {
      $result = $function($this->_resource, $file);
    }

    // Check result
    if( !$result ) {
      throw new Engine_Image_Adapter_Exception(sprintf("Unable to write image to file %s", $file));
    }

    return $this;
  }

  protected function _writeAnimated($file)
  {
    if( count($this->_frames) > 0 ) {
      $frames = array();
      foreach( $this->_frames as $nf ) {
        if( !is_resource($nf) ) {
          continue;
        }
        ob_start();
        imagegif($nf);
        $gifdata = ob_get_clean();
        array_push($frames, $gifdata);
        imagedestroy($nf);
      }
      $gifmerge = new GIF_Encoder(
        $frames, $this->_meta['delays'], $this->_meta['loops'], $this->_meta['disposal'], $this->_meta['tr'], $this->_meta['tg'], $this->_meta['tb'], 'bin'
      );
      $result = false === fwrite(fopen($file, 'wb'), $gifmerge->GetAnimation()) ? false : true;
    }
    return $this;
  }

  /**
   * Remove the current image object from memory
   */
  public function destroy()
  {
    if( $this->_isAnimatedGif() ) {
      foreach( $this->_frames as $nf ) {
        if( !is_resource($nf) ) {
          continue;
        }
        imagedestroy($nf);
      }
      return $this;
    }
    if( is_resource($this->_resource) ) {
      imagedestroy($this->_resource);
    }
    return $this;
  }

  /**
   * Output an image to buffer or return as string
   * 
   * @param string $type Image format
   * @param boolean $buffer Output or return?
   * @return mixed
   * @throws Engine_Image_Adapter_Exception If unable to output
   */
  public function output($buffer = false)
  {
    $this->_checkOpenImage();

    // Get output format
    $outputFormat = $this->_format;
    // Check support
    $function = 'image' . $outputFormat;
    if( !function_exists($function) ) {
      throw new Engine_Image_Adapter_Exception(sprintf('Image type "%s" is not supported', $outputFormat));
    }

    // Open buffer
    if( $buffer ) {
      ob_start();
    }

    // Apply quality
    $quality = null;
    if( is_int($this->_quality) && $this->_quality >= 0 && $this->_quality <= 100 ) {
      $quality = $this->_quality;
    }

    // Write
    if( $function == 'imagejpeg' && null !== $quality ) {
      $result = $function($this->_resource, null, $quality);
    } elseif( $function == 'imagepng' && null !== $quality ) {
      $result = $function($this->_resource, null, round(abs(($quality - 100) / 11.111111)));
    } else {
      $result = $function($this->_resource, null);
    }

    // Check result
    if( !$result ) {
      if( $buffer ) {
        ob_end_clean();
      }
      throw new Engine_Image_Adapter_Exception("Unable to output image");
    }

    // Finish
    if( $buffer ) {
      return ob_get_clean();
    } else {
      return $this;
    }
  }

  /**
   * Resizes current image to $width and $height. If aspect is set, will fit
   * within boundaries while keeping aspect
   * 
   * @param integer $width
   * @param integer $height
   * @param boolean $aspect (OPTIONAL) Default - true
   * @return Engine_Image_Adapter_Gd
   * @throws Engine_Image_Adapter_Exception If unable to resize
   */
  public function resize($width, $height, $aspect = true)
  {
    $this->_checkOpenImage();

    $imgW = $this->_width;
    $imgH = $this->_height;

    // Keep aspect
    if( $aspect ) {
      list($width, $height) = self::_fitImage($imgW, $imgH, $width, $height);
    }

    // Create new temporary image
    self::_isSafeToOpen($width, $height);
    if( $this->_isAnimatedGif() ) {
      $this->_resizeAnimated($width, $height);
    } else {
      $this->_resize($width, $height);
    }

    return $this;
  }

  /**
   * Crop an image
   *
   * @param integer $x
   * @param integer $y
   * @param integer $w
   * @param integer $h
   * @return Engine_Image_Adapter_Gd
   * @throws Engine_Image_Adapter_Exception If unable to crop
   */
  public function crop($x, $y, $w, $h)
  {
    $this->_checkOpenImage();

    // Create new temporary image and resize
    self::_isSafeToOpen($w, $h);
    if( $this->_isAnimatedGif() ) {
      $this->_cropAnimated($x, $y, $w, $h);
    } else {
      $this->_crop($x, $y, $w, $h);
    }


    return $this;
  }

  protected function _cropAnimated($cropX, $cropY, $cropWidth, $cropHeight)
  {
    $bg = null;
    $fullWidth = $this->_width;
    $fullHeight = $this->_height;
    $newFrames = array();
    $originalFrames = $this->_frames;
    foreach( $originalFrames as $k => $v ) {
      $frame = @imagecreatefromstring($v);
      if( !is_resource($frame) )
        continue;
      if( !is_resource($bg) ) {
        $bg = imagecreatetruecolor($fullWidth, $fullHeight);
        $this->_prepareGDimage($bg);
      }
      $srcX = 0;
      $srcY = 0;
      $srcW = imageSX($frame);
      $srcH = imageSY($frame);
      $dstX = $this->_originalFramesMeta[$k]['left'];
      $dstY = $this->_originalFramesMeta[$k]['top'];
      imagecopy($bg, $frame, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH);
      $newImg = imagecreatetruecolor($cropWidth, $cropHeight);
      $this->_prepareGDimage($newImg);
      imagecopy($newImg, $bg, 0, 0, $cropX, $cropY, $cropWidth, $cropHeight);
      array_push($newFrames, $newImg);
    }
    $this->_frames = $newFrames;
    return $this;
  }

  protected function _crop($x, $y, $w, $h)
  {
    $dst = imagecreatetruecolor($w, $h);

    // Try to preserve transparency
    self::_allocateTransparency($this->_resource, $dst, $this->_format);

    // Crop
    if( !imagecopy($dst, $this->_resource, 0, 0, $x, $y, $w, $h) ) {
      imagedestroy($dst);
      throw new Engine_Image_Adapter_Exception('Unable to crop image');
    }

    // Now destroy old image and overwrite with new
    imagedestroy($this->_resource);
    $this->_resource = $dst;
    $this->_width = $w;
    $this->_height = $h;
  }

  /**
   * Resample. Just crop+resize
   * 
   * @param integer $srcX
   * @param integer $srcY
   * @param integer $srcW
   * @param integer $srcH
   * @param integer $dstW
   * @param integer $dstH
   * @return Engine_Image_Adapter_Gd
   * @throws Engine_Image_Adapter_Exception If unable to crop
   */
  public function resample($srcX, $srcY, $srcW, $srcH, $dstW, $dstH)
  {
    $this->_checkOpenImage();

    // Create new temporary image
    self::_isSafeToOpen($dstW, $dstH);


    if( $this->_isAnimatedGif() ) {
      $this->_resampleAnimated($srcX, $srcY, $srcW, $srcH, $dstW, $dstH);
    } else {
      $this->_resample($srcX, $srcY, $srcW, $srcH, $dstW, $dstH);
    }
    return $this;
  }

  public function rotate($angle)
  {
    $this->_checkOpenImage();

    // Check if is safe to open (note if angle is not divisible by 90, then
    // this may not be handled correctly
    self::_isSafeToOpen($this->_width, $this->_height);
    if( $this->_isAnimatedGif() ) {
      $this->_rotateAnimated($angle);
    } else {
      $this->_rotate($angle);
    }


    return $this;
  }

  public function flip($horizontal = true)
  {
    $this->_checkOpenImage();

    // Create new temporary image
    self::_isSafeToOpen($this->_width, $this->_height);
    if( $this->_isAnimatedGif() ) {
      $this->_flipAnimated($horizontal);
    } else {
      $this->_flip($horizontal);
    }


    return $this;
  }

  protected function _resize($width, $height)
  {
    $imgW = $this->_width;
    $imgH = $this->_height;
    $dst = imagecreatetruecolor($width, $height);

    // Try to preserve transparency
    self::_allocateTransparency($this->_resource, $dst, $this->_format);

    // Resize
    if( !imagecopyresampled($dst, $this->_resource, 0, 0, 0, 0, $width, $height, $imgW, $imgH) ) {
      imagedestroy($dst);
      throw new Engine_Image_Adapter_Exception('Unable to resize image');
    }

    // Now destroy old image and overwrite with new
    imagedestroy($this->_resource);
    $this->_resource = $dst;
    $this->_width = $width;
    $this->_height = $height;
  }

  protected function _resizeAnimated($width, $height)
  {
    $bg = null;
    $ratio = 1.0;
    $fullWidth = $this->_width;
    $fullHeight = $this->_height;
    $finalWidth = $width;
    $finalHeight = $height;
    $ratioW = $fullWidth / $finalWidth;
    $ratioH = $fullHeight / $finalHeight;
    $ratio = ($ratioH > $ratioW ? $ratioH : $ratioW);
    $originalFrames = $this->_frames;
    $frames = array();
    foreach( $originalFrames as $k => $v ) {
      $frame = @imagecreatefromstring($v);
      if( !is_resource($frame) )
        continue;
      $newImg = imagecreatetruecolor($finalWidth, $finalHeight);
      $this->_prepareGDimage($newImg);
      if( is_resource($bg) ) {
        imagecopy($newImg, $bg, 0, 0, 0, 0, $finalWidth, $finalHeight);
      }
      $srcX = 0;
      $srcY = 0;
      $srcW = imageSX($frame);
      $srcH = imageSY($frame);
      $dstX = floor($this->_originalFramesMeta[$k]['left'] / $ratio);
      $dstY = floor($this->_originalFramesMeta[$k]['top'] / $ratio);
      $dstW = ceil($this->_originalFramesMeta[$k]['width'] / $ratio);
      $dstH = ceil($this->_originalFramesMeta[$k]['height'] / $ratio);
      imagecopyresampled($newImg, $frame, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH);
      array_push($frames, $newImg);
      if( !is_resource($bg) ) {
        $bg = imagecreatetruecolor($finalWidth, $finalHeight);
        $this->_prepareGDimage($bg);
      }
      imagecopy($bg, $newImg, 0, 0, 0, 0, $finalWidth, $finalHeight);
    }
    $this->_frames = $frames;
    $this->_width = $width;
    $this->_height = $height;
    return;
  }

  protected function _resample($srcX, $srcY, $srcW, $srcH, $dstW, $dstH)
  {
    $dst = imagecreatetruecolor($dstW, $dstH);
    // Try to preserve transparency
    self::_allocateTransparency($this->_resource, $dst, $this->_format);

    // Resample
    $result = imagecopyresampled($dst, $this->_resource, 0, 0, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH);

    if( !$result ) {
      imagedestroy($dst);
      throw new Engine_Image_Adapter_Exception('Unable to resample image');
    }

    // Now destroy old image and overwrite with new
    imagedestroy($this->_resource);
    $this->_resource = $dst;
    $this->_width = $dstW;
    $this->_height = $dstH;
  }

  protected function _resampleAnimated($srcX, $srcY, $srcW, $srcH, $dstW, $dstH)
  {
    // 4 = resize and crop from center with aspect ratio
    $bg = null;
    $bgX = $bgY = 0;
    $bgWidth = $this->_width;
    $bgHeight = $this->_height;
    $originalFrames = $this->_frames;
    $newFrames = array();
    foreach( $originalFrames as $k => $v ) {
      $frame = @imagecreatefromstring($v);
      if( !is_resource($frame) )
        continue;
      $newImg = imagecreatetruecolor($bgWidth, $bgHeight);
      $this->_prepareGDimage($newImg);
      if( is_resource($bg) ) {
        imagecopy($newImg, $bg, 0, 0, 0, 0, $bgWidth, $bgHeight);
      }
      imagecopyresampled($newImg, $frame, 0, 0, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH);
      if( !is_resource($bg) ) {
        $bg = imagecreatetruecolor($bgWidth, $bgHeight);
        $this->_prepareGDimage($bg);
      }
      imagecopy($bg, $newImg, 0, 0, 0, 0, $bgWidth, $bgHeight);
      $newImg = imagecreatetruecolor($dstW, $dstH);
      $this->_prepareGDimage($newImg);
      imagecopy($newImg, $bg, 0, 0, $bgX, $bgY, $dstW, $dstH);
      array_push($newFrames, $newImg);
      $originalFrames[$k] = null;
    }
    $this->_frames = $newFrames;
    $this->_width = $dstW;
    $this->_height = $dstH;
    return $this;
  }

  protected function _rotateAnimated($angle)
  {
    $originalFrames = $this->_frames;
    $newFrames = array();
    foreach( $originalFrames as $k => $v ) {
      $frame = @imagecreatefromstring($v);
      if( !is_resource($frame) )
        continue;
      $newImg = imagerotate($frame, $angle, 0);
      array_push($newFrames, $newImg);
    }
    $this->_frames = $newFrames;
    $this->_width = imagesx($newFrames[0]);
    $this->_height = imagesy($newFrames[0]);
  }

  protected function _rotate($angle)
  {
    // Rotate
    $result = imagerotate($this->_resource, $angle, 0);

    if( !$result ) {
      imagedestroy($result);
      throw new Engine_Image_Adapter_Exception('Unable to rotate image');
    }

    // Now destroy old image and overwrite with new
    imagedestroy($this->_resource);
    $this->_resource = $result;
    $this->_width = imagesx($this->_resource);
    $this->_height = imagesy($this->_resource);
    return $this;
  }

  protected function _flipAnimated($horizontal = true)
  {
    $originalFrames = $this->_frames;
    $newFrames = array();
    $bg = null;
    $bgX = $bgY = 0;
    $bgWidth = $this->_width;
    $bgHeight = $this->_height;
    foreach( $originalFrames as $k => $v ) {
      $frame = @imagecreatefromstring($v);
      if( !is_resource($frame) )
        continue;
      $newImg = imagecreatetruecolor($this->_width, $this->_height);
      $this->_prepareGDimage($newImg);
      if( is_resource($bg) ) {
        imagecopy($newImg, $bg, 0, 0, 0, 0, $bgWidth, $bgHeight);
      }

      if( $horizontal ) {
        $result = imagecopyresampled($newImg, $frame, 0, 0, ($this->_width - 1), 0, $this->_width, $this->_height, (0 - $this->_width), $this->_height);
      } else {
        $result = imagecopyresampled($newImg, $frame, 0, 0, 0, ($this->_height - 1), $this->_width, $this->_height, $this->_width, (0 - $this->_height));
      }
      if( !is_resource($bg) ) {
        $bg = imagecreatetruecolor($bgWidth, $bgHeight);
        $this->_prepareGDimage($bg);
      }
      imagecopy($bg, $newImg, 0, 0, 0, 0, $bgWidth, $bgHeight);
      $newImg = imagecreatetruecolor($this->_width, $this->_height);
      $this->_prepareGDimage($newImg);
      imagecopy($newImg, $bg, 0, 0, $bgX, $bgY, $this->_width, $this->_height);
      array_push($newFrames, $newImg);
      $originalFrames[$k] = null;
    }
    $this->_frames = $newFrames;
    $this->_width = imagesx($newFrames[0]);
    $this->_height = imagesy($newFrames[0]);
  }

  protected function _flip($horizontal = true)
  {

    $dst = imagecreatetruecolor($this->_width, $this->_height);

    // Try to preserve transparency
    self::_allocateTransparency($this->_resource, $dst, $this->_format);

    // Flip
    if( $horizontal ) {
      $result = imagecopyresampled($dst, $this->_resource, 0, 0, ($this->_width - 1), 0, $this->_width, $this->_height, (0 - $this->_width), $this->_height);
    } else {
      $result = imagecopyresampled($dst, $this->_resource, 0, 0, 0, ($this->_height - 1), $this->_width, $this->_height, $this->_width, (0 - $this->_height));
    }

    if( !$result ) {
      imagedestroy($result);
      throw new Engine_Image_Adapter_Exception('Unable to rotate image');
    }

    // Now destroy old image and overwrite with new
    imagedestroy($this->_resource);
    $this->_resource = $dst;
    $this->_width = imagesx($this->_resource);
    $this->_height = imagesy($this->_resource);
  }

  // Utility
  protected function _isAnimatedGif()
  {
    return 1 < preg_match_all('/\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)/s', $this->_getFileContent());
  }

  protected function _getFileContent()
  {
    if( $this->_fileContent == null ) {
      $this->_fileContent = file_get_contents($this->_file);
    }
    return $this->_fileContent;
  }

  protected function _prepareGDimage(&$gdimage)
  {
    if( !$this->_meta['trans'] )
      return;
    $transparentNew = imagecolorallocate($gdimage, $this->_meta['tr'], $this->_meta['tg'], $this->_meta['tb']);
    $transparentNewIndex = imagecolortransparent($gdimage, $transparentNew);
    imagefill($gdimage, 0, 0, $transparentNewIndex);
  }

  protected function _checkOpenImage($throw = true)
  {
    $isOpen = true;
    if( $this->_isAnimatedGif() ) {
      $isOpen = count(count($this->_originalFramesMeta) <= 0);
    } else {
      $isOpen = is_resource($this->_resource);
    }

    if( !$isOpen ) {
      if( $throw ) {
        throw new Engine_Image_Adapter_Exception('No open image to operate on.');
      } else {
        return false;
      }
    } else {
      return true;
    }
  }
  // Static

  /**
   * Check if it is safe to open an image (memory-wise)
   * 
   * @param integer $width Width in pixels
   * @param integer $height Height in pixels
   * @param integer $bpp Bytes per pixel
   */
  protected static function _isSafeToOpen($width, $height, $bpp = 4)
  {
    // "Fudge Factor"
    $fudge = 1.2;

    // Calculate used
    if( !function_exists('memory_get_usage') ) {
      $used = 15 * 1024 * 1024; // typical used
    } else {
      $used = memory_get_usage();
    }

    // Calculate limit
    $limit = false;
    if( function_exists('ini_get') ) {
      $limit = ini_get('memory_limit');
    }
    if( -1 == $limit ) {
      return true; // infinite mode
    } elseif( !$limit ) {
      $limit = 32 * 1024 * 1024; // recommended default
    } else {
      $limit = self::_convertBytes($limit);
    }

    // Calculate available and required
    $available = $limit - $used;
    $required = $width * $height * $bpp * $fudge;

    // Whoops, not enough memory
    if( $required > $available ) {
      throw new Engine_Image_Exception(sprintf('Insufficient memory to open ' .
        'image: %d required > %d available (%d limit, %d used)', $required, $available, $limit, $used));
    }
  }

  /**
   * Get supported format info
   * 
   * @return stdClass
   */
  protected static function getSupport()
  {
    if( null === self::$_support ) {
      $info = ( function_exists('gd_info') ? gd_info() : array() );
      $support = new stdClass();

      $support->freetype = !empty($info["FreeType Support"]);
      $support->t1lib = !empty($info["T1Lib Support"]);
      $support->gif = (!empty($info["GIF Read Support"]) && !empty($info["GIF Create Support"]) );
      $support->jpg = (!empty($info["JPG Support"]) || !empty($info["JPEG Support"]) );
      $support->jpeg = $support->jpg;
      $support->png = !empty($info["PNG Support"]);
      $support->wbmp = !empty($info["WBMP Support"]);
      $support->xbm = !empty($info["XBM Support"]);
      $support->bmp = true; // through b/c at bottom

      self::$_support = $support;
    }

    return self::$_support;
  }

  /**
   * Check if a specific image type is supported
   * 
   * @param string $type
   * @param boolean $throw
   * @return boolean
   * @throws Engine_Image_Adapter_Exception If $throw is true and not supported
   */
  protected static function _isSupported($type, $throw = true)
  {
    if( empty(self::getSupport()->$type) ) {
      if( $throw ) {
        throw new Engine_Image_Adapter_Exception(sprintf('Image type %s is not supported', $type));
      }
      return false;
    }
    return true;
  }

  /**
   * Convert short-hand bytes to integer
   * 
   * @param string $value
   * @return integer
   */
  protected static function _convertBytes($value)
  {
    if( is_numeric($value) ) {
      return $value;
    } else {
      $valueLength = strlen($value);
      $qty = substr($value, 0, $valueLength - 1);
      $unit = strtolower(substr($value, $valueLength - 1));
      switch( $unit ) {
        case 'k':
          $qty *= 1024;
          break;
        case 'm':
          $qty *= 1048576;
          break;
        case 'g':
          $qty *= 1073741824;
          break;
      }
      return $qty;
    }
  }

  protected static function _allocateTransparency(&$imgOne, &$imgTwo, $type)
  {
    // GIF
    if( $type == 'gif' ) {
      $transparentIndex = imagecolortransparent($imgOne);
      if( $transparentIndex >= 0 ) {
        $transparentColor = imagecolorsforindex($imgOne, $transparentIndex);
        $transparentIndexTwo = imagecolorallocate($imgTwo, $transparentColor['red'], $transparentColor['green'], $transparentColor['blue']);
        imagefill($imgTwo, 0, 0, $transparentIndexTwo);
        imagecolortransparent($imgTwo, $transparentIndexTwo);
      }
    } elseif( $type == 'png' ) { // PNG
      imagealphablending($imgTwo, false);
      $transparentColor = imagecolorallocatealpha($imgTwo, 0, 0, 0, 127);
      imagefill($imgTwo, 0, 0, $transparentColor);
      imagesavealpha($imgTwo, true);
    }
  }
}

if( !function_exists('imagecreatefrombmp') ) {

  function imagecreatefrombmp($filename)
  {
    if( !function_exists('imagecreatefromgd') ) {
      return false;
    }

    // Create tmp file
    $src = $filename;
    $dest = $tmpName = tempnam("/tmp", "GD");

    if( !($srcFile = fopen($src, "rb")) ) {
      return false;
    }

    if( !($srcFile = fopen($dest, "wb")) ) {
      return false;
    }

    $header = unpack("vtype/Vsize/v2reserved/Voffset", fread($srcFile, 14));
    $info = unpack("Vsize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vncolor/Vimportant", fread($srcFile, 40));

    extract($info);
    extract($header);

    if( $type != 0x4D42 ) {  // signature "BM"
      @fclose($srcFile);
      @fclose($srcFile);
      @unlink($tmpName);
      return false;
    }

    $paletteSize = $offset - 54;
    $ncolor = $paletteSize / 4;
    $gdHeader = "";
    // true-color vs. palette
    $gdHeader .= ( $paletteSize == 0) ? "\xFF\xFE" : "\xFF\xFF";
    $gdHeader .= pack("n2", $width, $height);
    $gdHeader .= ( $paletteSize == 0) ? "\x01" : "\x00";
    if( $paletteSize ) {
      $gdHeader .= pack("n", $ncolor);
    }
    // no transparency
    $gdHeader .= "\xFF\xFF\xFF\xFF";

    fwrite($srcFile, $gdHeader);

    if( $paletteSize ) {
      $palette = fread($srcFile, $paletteSize);
      $gdPalette = "";
      $j = 0;
      while( $j < $paletteSize ) {
        $b = $palette{$j++};
        $g = $palette{$j++};
        $r = $palette{$j++};
        $a = $palette{$j++};
        $gdPalette .= "$r$g$b$a";
      }
      $gdPalette .= str_repeat("\x00\x00\x00\x00", 256 - $ncolor);
      fwrite($srcFile, $gdPalette);
    }

    $scanLineSize = (($bits * $width) + 7) >> 3;
    $scanLineAlign = ($scanLineSize & 0x03) ? 4 - ($scanLineSize & 0x03) : 0;

    for( $i = 0, $l = $height - 1; $i < $height; $i++, $l-- ) {
      // BMP stores scan lines starting from bottom
      fseek($srcFile, $offset + (($scanLineSize + $scanLineAlign) * $l));
      $scanLine = fread($srcFile, $scanLineSize);
      if( $bits == 24 ) {
        $gdScanLine = "";
        $j = 0;
        while( $j < $scanLineSize ) {
          $b = $scanLine{$j++};
          $g = $scanLine{$j++};
          $r = $scanLine{$j++};
          $gdScanLine .= "\x00$r$g$b";
        }
      } elseif( $bits == 8 ) {
        $gdScanLine = $scanLine;
      } elseif( $bits == 4 ) {
        $gdScanLine = "";
        $j = 0;
        while( $j < $scanLineSize ) {
          $byte = ord($scanLine{$j++});
          $p = array();
          $p[] = chr($byte >> 4);
          $p[] = chr($byte & 0x0F);
          $gdScanLine .= join('', $p);
        }
        $gdScanLine = substr($gdScanLine, 0, $width);
      } elseif( $bits == 1 ) {
        $gdScanLine = "";
        $j = 0;
        while( $j < $scanLineSize ) {
          $byte = ord($scanLine{$j++});
          $p = array();
          $p[] = chr((int) (($byte & 0x80) != 0));
          $p[] = chr((int) (($byte & 0x40) != 0));
          $p[] = chr((int) (($byte & 0x20) != 0));
          $p[] = chr((int) (($byte & 0x10) != 0));
          $p[] = chr((int) (($byte & 0x08) != 0));
          $p[] = chr((int) (($byte & 0x04) != 0));
          $p[] = chr((int) (($byte & 0x02) != 0));
          $p[] = chr((int) (($byte & 0x01) != 0));
          $gdScanLine .= join('', $p);
        }
        $gdScanLine = substr($gdScanLine, 0, $width);
      }

      fwrite($srcFile, $gdScanLine);
    }

    fclose($srcFile);
    fclose($srcFile);

    // Create from GD
    $img = imagecreatefromgd($tmpName);
    @unlink($tmpName);
    return $img;
  }
}