<?php
/**
 * A class for manipulating images.
 *
 * This class provides a way to open an image file and either rotate, flip,
 * resize, crop or watermark it.
 */
class image
{
    /**
     * @var integer The watermark options.
     */
    const CENTER        = 0;
    const TOP_CENTER    = 1;
    const TOP_RIGHT     = 2;
    const CENTER_RIGHT  = 3;
    const BOTTOM_RIGHT  = 4;
    const BOTTOM_CENTER = 5;
    const BOTTOM_LEFT   = 6;
    const CENTER_LEFT   = 7;
    const TOP_LEFT      = 8;

    /**
     * @var string The filename.
     */
    protected $file;

    /**
     * @var resource The image resource (gd).
     */
    protected $resource;

    ////////////////////////////////////////////////////////////////////////////
    // Construct and destruct functions.

    /**
     * @param integer (optional) $width The width.
     * @param integer (optional) $height The height.
     * @param boolean (optional) $true_color Whether the image is true color or
     * not.
     * -
     * @param resource (optional) $width The image resource (gd).
     * -
     * @param string (optional) $width The image file.
     */
    public function __construct($width = null, $height = null, $true_color = true)
    {
        if ($width !== null)
        {
            $this->open($width, $height, $true_color);
        }
    }

    public function __destruct()
    {
        $this->close();
    }

    ////////////////////////////////////////////////////////////////////////////
    // Overload functions.
    
    /**
     * Gets the image width or height.
     * 
     * @param string $name The side.
     * @throws logicexception If the image is not opened.
     * @throws outofrangeexception If the property does not exist.
     * @throws unexpectedvalueexception If the image size could not be retrieved.
     * @return mixed The value.
     */
    public function __get($name)
    {
        if (!$this->resource)
        {
            throw new logicexception('Image is not opened.');
        }
        
        switch ($name)
        {
            case 'width'  :
            case 'w'      :
            case 'x'      : $function = 'imagesx'; break;
            case 'height' :
            case 'h'      :
            case 'y'      : $function = 'imagesy'; break;
            default       : throw new outofrangeexception('Property ' . $name . ' does not exist.');
        }
        
        $size = $function($this->resource);
        
        if ($size === false)
        {
            throw new unexpectedvalueexception('Size could not be retrieved.');
        }
        
        return $size;
    }
    
    /**
     * Gets the image resource.
     *
     * @throws logicexception If the image is not opened.
     * @return resource The resource.
     */
    public function __toresource()
    {
        if (!$this->resource)
        {
            throw new logicexception('Image is not opened.');
        }
        
        return $this->resource;
    }

    ////////////////////////////////////////////////////////////////////////////
    // Interface functions.

    /**
     * Opens an image from a file or a blank one using width and height.
     *
     * @param integer $width The width.
     * @param integer (optional) $height The height.
     * @param boolean (optional) $true_color Whether the image is true color or
     * not.
     * @throws domainexception If the width or height is negative.
     * @throws unexpectedvalueexception If the image could not be opened.
     * -
     * @param resource $resource The image resource.
     * -
     * @param string $file The image file.
     * @throws logicexception If the image extension is not supported.
     * @throws unexpectedvalueexception If the image could not be opened.
     * @return image A fluent interface.
     */
    public function open($width, $height = null, $true_color = true)
    {
        $this->close();
        $resource = null;
        
        if ($height !== null)
        {
            if ($width < 1 || $height < 1)
            {
                throw new domainexception('Width and/or height (' . var_export($width, true) . ',' . var_export($height, true) . ') cannot be below 1.');
            }
            
            $function = 'imagecreate' . (($true_color) ? 'truecolor' : '');
            $resource = $function($width, $height);
        }
        elseif (is_resource($width))
        {
            $type = get_resource_type($width);
            
            if ($type !== 'gd')
            {
                throw new invalidargumentexception('Argument 1 passed to ' . __METHOD__ . '() must be an image resource, ' . $type . ' resource given.');
            }
            
            $resource = $width;
        }
        else
        {
            if (!is_file($width))
            {
                throw new logicexception('File ' . var_export($width, true) . ' is missing.');
            }
            
            $extension = strtolower(pathinfo($width, PATHINFO_EXTENSION));
            
            if ($extension === 'jpg')
            {
                $extension = 'jpeg';
            }
            
            $function = 'imagecreatefrom' . $extension;
            
            if (!function_exists($function))
            {
                throw new logicexception('Extension ' . $extension . ' is not supported.');
            }
            
            $resource = $function($width);
            
            if ($resource)
            {
                $this->file = $width;
            }
        }
        
        if (!$resource)
        {
            throw new unexpectedvalueexception('Image could not be opened.');
        }
        
        $this->resource = $resource;
        return $this;
    }

    /**
     * Saves an image to file.
     * 
     * @param string (optional) $file The filename.
     * @param integer (optional) $quality The jpeg compression quality.
     * @throws logicexception If the image is not opened.
     * @throws logicexception If the image filename is not defined.
     * @throws logicexception If the image extension is not supported.
     * @throws unexpectedvalueexception If the image could not be saved to file.
     * @return image A fluent interface.
     */
    public function save($file = null, $quality = null)
    {
        if (!$this->resource)
        {
            throw new logicexception('Image is not opened.');
        }
        
        if ($file === null)
        {
            if (!$this->file)
            {
                throw new logicexception('File is not defined.');
            }
            
            $file = $this->file;
        }
        
        $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
        
        if ($extension === 'jpg')
        {
            $extension = 'jpeg';
        }
        
        $function = 'image' . $extension;
        
        if (!function_exists($function))
        {
            throw new logicexception('Extension ' . $extension . ' is not supported.');
        }
        
        $parameter = array($this->resource, $file);
        
        if ($extension === 'jpeg' && $quality !== null)
        {
            $parameter[] = $quality;
        }
        
        if (!call_user_func_array($function, $parameter))
        {
            throw new unexpectedvalueexception('Image could not be saved to file ' . var_export($file, true) . '.');
        }
        
        return $this;
    }

    /**
     * Renders the image.
     * 
     * @param string (optional) $extension The extension.
     * @throws logicexception If the image is not opened.
     * @throws logicexception If the image extension is not supported.
     * @throws unexpectedvalueexception If the output buffer could not be
     * activated or retrieved.
     * @throws unexpectedvalueexception If the image could not be rendered.
     * @return string The rendered image.
     */
    public function render($extension = null)
    {
        if (!$this->resource)
        {
            throw new logicexception('Image is not opened.');
        }
        
        if ($extension === null)
        {
            $extension = pathinfo($this->file, PATHINFO_EXTENSION);
        }
        
        $extension = strtolower($extension);
        
        if (!$extension || $extension === 'jpg')
        {
            $extension = 'jpeg';
        }
        
        $function = 'image' . $extension;
        
        if (!function_exists($function))
        {
            throw new logicexception('Extension ' . $extension . ' is not supported.');
        }
        
        if (!ob_start())
        {
            throw new unexpectedvalueexception('Output buffer could not be activated.');
        }
        
        if (!$function($this->resource))
        {
            throw new unexpectedvalueexception('Image could not be rendered.');
        }
        
        $content = ob_get_clean();
        
        if ($content === false)
        {
            throw new unexpectedvalueexception('Output buffer could not be retrieved.');
        }
        
        return $content;
    }
    
    /**
     * Closes the image.
     *
     * @throws unexpectedvalueexception If the image could not be closed.
     * @return image A fluent interface.
     */
    public function close()
    {
        if ($this->resource)
        {
            if (!imagedestroy($this->resource))
            {
                throw new unexpectedvalueexception('Image could not be closed.');
            }
        }
        
        $this->resource = null;
        $this->file = null;
        return $this;
    }

    /**
     * Rotates the image.
     * 
     * @param integer $angle The rotation angle.
     * @throws logicexception If the image is not opened.
     * @throws unexpectedvalueexception If the image could not be rotated.
     * @return image A fluent interface.
     */
    public function rotate($angle)
    {
        if (!$this->resource)
        {
            throw new logicexception('Image is not opened.');
        }
        
        $angle = $angle % 360;
        $rotated = imagerotate($this->resource, $angle, 0);
        
        if (!$rotated)
        {
            throw new unexpectedvalueexception('Image could not be rotated ' . var_export($angle, true) . ' degrees.');
        }
        
        $this->resource = $rotated;
        return $this;
    }

    /**
     * Resizes the image.
     * 
     * @param integer $new_width The width.
     * @param integer $new_height The height.
     * @param boolean (optional) $proportion Whether to keep or force proportions.
     * @throws logicexception If the image is not opened.
     * @throws domainexception If the width or height is negative.
     * @throws unexpectedvalueexception If the resized image could not be created.
     * @throws unexpectedvalueexception If the image could not be resized.
     * @return image A fluent interface.
     */
    public function resize($new_width, $new_height, $proportion = true)
    {
        if (!$this->resource)
        {
            throw new logicexception('Image is not opened.');
        }
        
        $current_width = $this->width;
        $current_height = $this->height;
        
        if ($new_width === null)
        {
            if ($new_height < 1)
            {
                throw new domainexception('Height ' . var_export($new_height, true) . ' cannot be below 1.');
            }
            
            $new_width = ($proportion) ? max(round($current_width * $new_height / $current_height), 1) : $current_width;
        }
        elseif ($new_height === null)
        {
            if ($new_width < 1)
            {
                throw new domainexception('Width ' . var_export($new_width, true) . ' cannot be below 1.');
            }
            
            $new_height = ($proportion) ? max(round($current_height * $new_width / $current_width), 1) : $current_height;
        }
        else
        {
            if ($new_width < 1 || $new_height < 1)
            {
                throw new domainexception('Width and/or height (' . var_export($new_width, true) . ',' . var_export($new_height, true) . ') cannot be below 1.');
            }
            
            if ($proportion)
            {
                if (($current_width / $new_width) > ($current_height / $new_height))
                {
                    $new_height = max(round($current_height * $new_width / $current_width), 1);
                }
                elseif (($current_width / $new_width) < ($current_height / $new_height))
                {
                    $new_width = max(round($current_width * $new_height / $current_height), 1);
                }
            }
        }
        
        $function = 'imagecreate' . ((imageistruecolor($this->resource)) ? 'truecolor' : '');
        $resized = $function($new_width, $new_height);
        
        if (!$resized)
        {
            throw new unexpectedvalueexception('Resized image could not be created.');
        }
        
        if (!imagecopyresampled($resized, $this->resource, 0, 0, 0, 0, $new_width, $new_height, $current_width, $current_height))
        {
            throw new unexpectedvalueexception('Image could not be resized to (' . var_export($new_width, true) . ',' . var_export($new_height, true) . ').');
        }
        
        $this->resource = $resized;
        return $this;
    }

    /**
     * Watermarks the image.
     * 
     * @param string/image $watermark The watermark filename or image object.
     * @param integer (optional) $position The watermark position.
     * @param integer (optional) $x The horizontal offset.
     * @param integer (optional) $y The vertical offset.
     * @throws logicexception If the image is not opened.
     * @throws outofrangeexception If the position code does not exist.
     * @throws unexpectedvalueexception If alpha blending could not be
     * initiated.
     * @throws unexpectedvalueexception If the watermark image could not be
     * placed.
     * @return image A fluent interface.
     */
    public function watermark($watermark, $position = self::CENTER, $x = 10, $y = 10)
    {
        if (!$this->resource)
        {
            throw new logicexception('Image is not opened.');
        }
        
        if (!$watermark instanceof image)
        {
            $watermark = new image($watermark);
        }
        
        $iw = $this->width;
        $ih = $this->height;
        $ww = $watermark->width;
        $wh = $watermark->height;
        $ix = $iy = 0;
        
        switch ($position)
        {
            case self::CENTER        : $ix = ($iw - $ww) / 2;  $iy = ($ih - $wh) / 2;  break;
            case self::TOP_CENTER    : $ix = ($iw - $ww) / 2;  $iy = $y;               break;
            case self::TOP_RIGHT     : $ix = $iw - ($ww + $x); $iy = $y;               break;
            case self::CENTER_RIGHT  : $ix = $iw - ($ww + $x); $iy = ($ih - $wh) / 2;  break;
            case self::BOTTOM_RIGHT  : $ix = $iw - ($ww + $x); $iy = $ih - ($wh + $y); break;
            case self::BOTTOM_CENTER : $ix = ($iw - $ww) / 2;  $iy = $ih - ($wh + $y); break;
            case self::BOTTOM_LEFT   : $ix = $x;               $iy = $ih - ($wh + $y); break;
            case self::CENTER_LEFT   : $ix = $x;               $iy = ($ih - $wh) / 2;  break;
            case self::TOP_LEFT      : $ix = $x;               $iy = $y;               break;
            default                  : throw new outofrangeexception('Position code ' . $position . ' does not exist.');
        }
        
        $ix = round($ix);
        $iy = round($iy);
        
        if (!imagealphablending($this->resource, true))
        {
            throw new unexpectedvalueexception('Alpha blending could not be initiated.');
        }
        
        if (!imagecopy($this->resource, $watermark->__toresource(), $ix, $iy, 0, 0, $ww, $wh))
        {
            throw new unexpectedvalueexception('Watermark could not be placed.');
        }
        
        return $this;
    }

    ////////////////////////////////////////////////////////////////////////////
    // Internal functions.
}
