Файл: toolkit/AcImage.php
Строк: 679
<?php
/**
 * @package image
 *
 * @author Андрей Загорцев <freeron@ya.ru>
 * @author Антон Кургузенков <kurguzenkov@list.ru>
 *
 * @version 2.0.1
 * @since 2013-03-12
 */
require_once 'geometry/Rectangle.php';
require_once 'geometry/exceptions.php';
require_once 'AcColor.php';
require_once 'AcImageJPG.php';
require_once 'AcImageGIF.php';
require_once 'AcImagePNG.php';
/**
 * Класс, описывающий изображение, и содержащий методы для работы с ним.
 */
class AcImage
{
    const PNG = 'image/png';
    const JPEG = 'image/jpeg';
    const GIF = 'image/gif';
    const PROPORTION = 'pr';
    const PIXELS = 'px';
    const PERCENT = '%';
    const TOP_LEFT = 0;
    const TOP_RIGHT = 1;
    const BOTTOM_RIGHT = 2;
    const BOTTOM_LEFT = 3;
    private static $correctCorners = array (0, 1, 2, 3);
    private static $cornerLogo = 2; // BOTTOM_RIGHT
    /**
     * Путь к файлу с изображением
     *
     * @var string
     */
    private $filePath;
    /**
     * Размер (высота и ширина) изображения
     *
     * @var Size
     */
    private $size;
    /**
     * Массив с информацией об изображении,
     * который возвращаыет функция getimagesize
     *
     * @var array
     */
    private $imageInfo;
    /**
     * Ресурс изображения
     */
    private $resource;
    /**
     * Цвет фона по умолчанию
     * @var int|AcColor
     */
    private static $backgroundColor = AcColor::WHITE;
    /**
     * Качество по умолчанию
     *
     * @var int
     */
    private static $quality = 85;
    /**
     * Вкл/откл прозрачность
     * @var boolean
     */
    private static $transparency = true;
    /**
     * Информация о поддерживаемых типах изображений
     *
     * @var array
     */
    private static $gdInfo;
    /**
     * Разрешить перезаписывание существующих файлов
     * @var boolean
     */
    private static $rewrite = false;
    /**
     * Какую часть максимальную часть лого может занимать от стороны исходного изображения
     * @var float
     */
    private static $maxProportionLogo = 0.1;
    private static $paddingProportionLogo = 0.02;
    /**
     * @param string путь к файлу с изображением
     */
    protected function __construct($filePath)
    {
        if (!self::isFileExists($filePath))
            throw new FileNotFoundException();
        $this->filePath = $filePath;
        $imageInfo = $this->getImageInfo();
        if (!is_array($imageInfo))
            throw new InvalidFileException($filePath);
        $this->setSize(new Size($imageInfo[0], $imageInfo[1]));
    }
    /**
     * Cоздаёт экземпляры классов AcImageJPG, AcImageGIF, AcImagePNG
     * в зависимости от типа изображения.
     *
     * @param string путь к файлу с изображением
     * @return AcImageJPG|AcImagePNG|AcImageGIF
     */
    public static function createImage($filePath)
    {
        $image = new AcImage($filePath);
        if (!self::isSupportedGD())
            throw new GDnotInstalledException();
        $imageInfo = $image->getImageInfo();
        if (!is_array($imageInfo))
            throw new InvalidFileException($filePath);
        $mimeType = $imageInfo['mime'];
        switch ($mimeType)
        {
            case self::JPEG :
                return new AcImageJPG($filePath);
            case self::PNG :
                return new AcImagePNG($filePath);
            case self::GIF :
                return new AcImageGIF($filePath);
            default:
                throw new InvalidFileException($filePath);
        }
    }
    /**
     * Сохраняет изображение в формате jpg
     *
     * @param string путь, по которому сохранится изображение
     * @return AcImage
     *
     * @throws UnsupportedFormatException
     * @throws FileAlreadyExistsException
     * @throws FileNotSaveException
     */
    public function saveAsJPG($path)
    {
        if(!AcImageJPG::isSupport())
            throw new UnsupportedFormatException();
        if (!self::getRewrite() && self::isFileExists($path))
            throw new FileAlreadyExistsException($path);
        $this->putBackground(self::$backgroundColor);
        if(!imagejpeg(self::getResource(), $path, self::getQuality()))
            throw new FileNotSaveException($path);
        return $this;
    }
    /**
     * Сохраняет изображение в формате png
     *
     * @param string путь, по которому сохранится изображение
     * @return AcImage
     *
     * @throws UnsupportedFormatException
     * @throws FileAlreadyExistsException
     * @throws FileNotSaveException
     */
    public function saveAsPNG($path)
    {
        if(!AcImagePNG::isSupport())
            throw new UnsupportedFormatException('png');
        if (!self::getRewrite() && self::isFileExists($path))
            throw new FileAlreadyExistsException($path);
        if (!self::getTransparency())
            $this->putBackground(self::$backgroundColor);
        // php >= 5.1.2
        if(!imagePng(self::getResource(), $path, AcImagePNG::getQuality()))
            throw new FileNotSaveException($path);
        return $this;
    }
 /**
     * Сохраняет изображение в формате GIF
     *
     * @param string путь, по которому сохранится изображение
     * @return AcImage
     *
     * @throws UnsupportedFormatException
     * @throws FileAlreadyExistsException
     * @throws FileNotSaveException
     */
    public function saveAsGIF($path)
    {
        if(!AcImageGIF::isSupportedGD())
            throw new UnsupportedFormatException();
        if (!self::getRewrite() && self::isFileExists($path))
            throw new FileAlreadyExistsException($path);;
        if(!self::getTransparency())
            $this->putBackground(self::$backgroundColor);
        if(!imagegif(self::getResource(), $path))
            throw new FileNotSaveException($path);
        return $this;
    }
    /**
     * Вписывает изображение в рамки.
     * Принимает размер (рамку) в которую вписывается изображение
     * или высоту и ширину этой рамки.
     *
     * @return AcImage
     * @throws IllegalArgumentException
     */
    public function resize() // Size $s || $width, $height
    {
        $args = func_get_args();
        if (count($args) == 2) // $width, $height
        {
            return $this->resize(new Size($args[0], $args[1]));
        }
        else if (count($args) == 1 && $args[0] instanceof Size)
        {
            $size = $args[0];
            $imageSize = $this->getSize()->getByFrame($size);
            $newImageResource = imagecreatetruecolor($imageSize->getWidth(), $imageSize->getHeight());
            imageAlphaBlending($newImageResource, false);
            imageSaveAlpha($newImageResource, true);
            imagecopyresampled($newImageResource, $this->getResource(), 0, 0, 0, 0, $imageSize->getWidth(),
                $imageSize->getHeight(), $this->getWidth(), $this->getHeight());
            $this->setResource($newImageResource);
            $this->setSize($imageSize);
            return $this;
        }
        throw new IllegalArgumentException();
    }
    /**
     * Ужимает изображение по <i>ширине</i>.
     *
     * @param int ширина рамки
     * @return AcImage
     * @throws IllegalArgumentException
     */
    public function resizeByWidth($width)
    {
        if (!is_int($width) || $width <= 0)
            throw new IllegalArgumentException();
        return $this->resize($width, $this->getHeight());
    }
    /**
     * Ужимает изображение по <i>высоте</i>.
     *
     * @param int высота рамки
     * @return AcImage
     * @throws IllegalArgumentException
     */
    public function resizeByHeight($height)
    {
        if (!is_int($height) || $height <= 0)
            throw new IllegalArgumentException();
        return $this->resize($this->getWidth(), $height);
    }
    /**
     * Производит кроп изображения, то есть
     * вырезает из него произвольную прямоугольную область.
     * Метод принимает либо вырезаемый прямоугольник, либо его параметры.
     *
     * @return AcImage
     * @throws IllegalArgumentException
     */
    public function crop() // Rectangle $rect || $x, $y, $w, $h
    {
        $a = func_get_args();
        if (count($a) == 4)
            $rect = new Rectangle($a[0], $a[1], $a[2], $a[3]);
        else if (count($a) == 1 && $a[0] instanceof Rectangle)
            $rect = $a[0];
        $rect = $rect->getIntersectsWith(new Rectangle(new Point(0, 0), $this->getSize()));
        if (!$rect)
            throw new IllegalArgumentException();
        $width = $rect->getWidth();
        $height = $rect->getHeight();
        $x = $rect->getLeft();
        $y = $rect->getTop();
        if ($width == 0 || $height == 0)
            throw new IllegalArgumentException();
        $newImageResource = imagecreatetruecolor($width, $height);
        imageAlphaBlending($newImageResource, false);
        imageSaveAlpha($newImageResource, true);
        imagecopyresampled($newImageResource, $this->getResource(), 0, 0, $x, $y, $width, $height, $width, $height);
        $this->setResource($newImageResource);
        $this->setSize($rect->getSize());
        return $this;
    }
    /**
     * Вырезает произвольный квадрат из изображения.
     * Метод может принимать вырезаемый прямоугольник, обязанный быть квадратом,
     * либо параметры для создания такого прямоугольника.
     *
     * @throws IllegalArgumentException
     * @return AcImage
     */
    public function cropSquare() // Rectnagle $square || Point $p, $a || $x, $y, $a
    {
        $a = func_get_args();
        if (count($a) == 1 && $a[0] instanceof Rectangle && $a[0]->isSquare())
        {
            $square = $a[0];
        }
        else if (count($a) == 2 && $a[0] instanceof Point && is_int($a[1]))
        {
            $square = new Rectangle($a[0], new Size($a[1], $a[1]));
        }
        else if(count($a) == 3)
        {
            $square = new Rectangle(new Point($a[0], $a[1]), new Size($a[2], $a[2]));
        }
        else
        {
            throw new IllegalArgumentException();
        }
        if (!$square->isInner(new Rectangle(new Point(0, 0), $this->getSize())))
            throw new IllegalArgumentException();
        return $this->crop($square);
    }
    /**
     * Вырезает центральную область изображения.
     * Принимает высоту и ширину вырезаемой области.
     *
     * @param int|string ширина вырезаемой области
     * @param int|string высота вырезаемой области
     *
     * @return AcImage
     * @throws IllegalArgumentException
     */
    public function cropCenter($width, $height) // int, int || string, string || int, string || string, int
    {
        $result = self::parseCropCenterArg($width);
        $widthUnits = $result['units'];
        $width = $result['value'];
        $result = self::parseCropCenterArg($height);
        $heightUnits = $result['units'];
        $height = $result['value'];
        // true тогда и только тогда если слева и справа от xor разные значения
        if ($widthUnits == self::PROPORTION xor $heightUnits == self::PROPORTION)
            throw new IllegalArgumentException();
        if ($widthUnits == self::PERCENT)
            $width = self::percentToPixels($width, $this->getWidth());
        if ($heightUnits == self::PERCENT)
            $height = self::percentToPixels($height, $this->getHeight());
        if ($widthUnits == self::PROPORTION)
        {
            $size = $this->getSizeByProportion($width, $height);
            $width = $size->getWidth();
            $height = $size->getHeight();
        }
        $width = (int)min($width, $this->getWidth());
        $height = (int)min($height, $this->getHeight());
        $imageRect = new Rectangle(0, 0, $this->getWidth(), $this->getHeight());
        $cropRect = new Rectangle(0, 0, $width, $height);
        $cropRect = $cropRect->center($imageRect);
        return $this->crop($cropRect);
    }
    /**
     * @ignore
     */
    private static function percentToPixels($value, $imageSide)
    {
        return (int)round(($imageSide / 100 * $value));
    }
    /**
     * @ignore
     */
    private function getSizeByProportion($width, $height)
    {
        $imageWidth = $this->getWidth();
        $imageHeight = $this->getHeight();
        if($height / $imageHeight > $width / $imageWidth)
        {
            return new Size((int)round($imageHeight / $height * $width), $imageHeight);
        }
        else
        {
            return new Size($imageWidth, (int)round($imageWidth / $width * $height));
        }
    }
    /**
     * @ignore
     */
    private static function parseCropCenterArg($arg)
    {
        $pattern = '/^(d+(.d+)*)(px|%|pr)$/'; // in constants?
        $matches = array();
        if (is_int($arg))
        {
            $units = self::PIXELS;
            $value = $arg;
        }
        else if (preg_match($pattern, $arg, $matches))
        {
            $units = $matches[3]; // (px|%|pr)
            $value = $matches[1]; // (d+(.d+)*)
        }
        else
        {
            throw new IllegalArgumentException();
        }
        return array (
            'units' => $units,
            'value' => $value
        );
    }
    /**
     * "Умное" создание миниатюр.
     *
     * @param int ширина
     * @param int высота
     * @param float коэффициент превышения.
     * @throws IllegalArgumentException
     * @return AcImage
     */
    public function thumbnail($width, $height, $c = 2) // $width, $height, [$c]
    {
        if ($c <= 1 || !is_finite($c) || $width <= 0 || $height <= 0)
            throw new IllegalArgumentException();
        $size = new Size($width, $height);
        if($this->getSize()->lessThen($size))
            $size = $this->getSize();
        $imageSize = $this->getSize();
        $isRotate = false;
        if($size->getWidth() / $imageSize->getWidth() <= $size->getHeight() / $imageSize->getHeight())
        {
            $size->flip();
            $imageSize->flip();
            $isRotate = true;
        }
        $width = $size->getWidth();
        $height = $size->getHeight();
        $imageWidth = $imageSize->getWidth();
        $imageHeight = $imageSize->getHeight();
        $lim = (int)($c * $height);
        $newHeight = (int)($imageHeight * $width / $imageWidth);
        if ($imageWidth > $width)
        {
            if($newHeight <= $lim)
            {
                $size = new Size($width, $newHeight);
            }
            else
            {
                if($newHeight <= 2 * $lim)
                {
                    $size = new Size((int)($imageWidth * $lim / $imageHeight), $lim);
                }
                else
                {
                    $size = new Size((int)($width / 2), (int)($imageHeight * $width / $imageWidth));
                }
            }
        }
        else
        {
            if($imageHeight <= $lim)
            {
                $size = $this->getSize();
            }
            else
            {
                if($imageHeight <= 2 * $lim)
                {
                    if($imageWidth * $lim / $imageHeight >= $width / 2)
                    {
                        $size = new Size((int)($imageWidth * $lim / $imageHeight), $lim);
                    }
                    else
                    {
                        $size = new Size((int)($width / 2), (int)($imageHeight * $width / ($imageWidth * 2)));
                    }
                }
                else
                {
                    $size = new Size((int)($width / 2), (int)($imageHeight * $width / ($imageWidth * 2)));
                }
            }
        }
        if ($isRotate)
        {
            $size->flip();
            $imageSize->flip();
        }
        return $this->resize($size);
    }
    /**
     * Наносит лого на изображение.
     *
     * @param mixed
     * @param int номер угла, в котором будет размещенно лого<br />
     * 0 1<br />
     * 2 3
     *
     * @see AcImage::TOP_LEFT
     * @see AcImage::TOP_RIGHT
     * @see AcImage::BOTTOM_LEFT
     * @see AcImage::BOTTOM_RIGHT
     *
     * @return AcImage
     * @throws IllegalArgumentException
     */
    public function drawLogo($logo, $corner = null) // string $logo [$corner] || AcImage $logo [$corner]
    {
        if (is_null($corner))
            $corner = self::$cornerLogo;
        if (!AcImage::isCorrectCorner($corner))
            throw new IllegalArgumentException();
        if (is_string($logo))
            $logo = AcImage::createImage($logo);
        if (!($logo instanceof AcImage))
            throw new IllegalArgumentException();
        $maxWidthLogo = (int)($this->getWidth() * self::$maxProportionLogo);
        $maxHeightLogo = (int)($this->getHeight() * self::$maxProportionLogo);
        $logo->resize($maxWidthLogo, $maxHeightLogo);
        if (!self::getTransparency())
            $logo->putBackground(self::$backgroundColor);
        imagealphablending($this->getResource(), true);
        $logoSize = $logo->getSize();
        $location = $this->getLogoPosition($corner, $logoSize->getWidth(), $logoSize->getHeight());
        imagecopy($this->getResource(), $logo->getResource(), $location->getX(), $location->getY(), 0, 0,
                        $logoSize->getWidth(), $logoSize->getHeight());
        return $this;
    }
    /**
     * @ignore
     */
    private function getLogoPosition($corner, $width, $height)
    {
        $paddingX = $this->getWidth() * self::$paddingProportionLogo;
        $paddingY = $this->getHeight() * self::$paddingProportionLogo;
        if ($corner == self::BOTTOM_RIGHT || $corner == self::BOTTOM_LEFT)
            $y = $this->getHeight() - $paddingY - $height;
        else
            $y = $paddingY;
        if ($corner == self::BOTTOM_RIGHT  || $corner == self::TOP_RIGHT)
            $x = $this->getWidth() - $paddingX - $width;
        else
            $x = $paddingX;
        return new Point((int)$x, (int)$y);
    }
    /**
     * @ignore
     */
    private static function isCorrectCorner($corner)
    {
        return in_array($corner, self::$correctCorners);
    }
    /**
     * Проверяет, существует ли файл.
     *
     * @param string путь к файлу
     * @return boolean
     */
    public static function isFileExists($filePath)
    {
        if (@file_exists($filePath))
            return true;
        if(!preg_match("|^http(s)?|", $filePath))
            return false;
        $headers = @get_headers($filePath);
        if(preg_match("|200|", $headers[0]))
            return true;
        return false;
    }
    /**
     * Проверяет, является ли файл изображением.
     *
     * @param string путь к файлу
     * @return boolean
     */
    public static function isFileImage($filePath)
    {
        if (!self::isFileExists($filePath))
            return false;
        $imageInfo = @getimagesize($filePath);
        return is_array($imageInfo);
    }
    /**
     * @ignore
     */
    protected function putBackground()
    {
        $newImageResource = imagecreatetruecolor($this->getWidth(), $this->getHeight());
        imagefill($newImageResource , 0, 0, self::getBackgroundColor()->getCode());
        imagecopyresampled($newImageResource , $this->getResource(), 0, 0, 0, 0,
            $this->getWidth(), $this->getHeight(), $this->getWidth(), $this->getHeight());
        $this->setResource($newImageResource);
    }
    /**
     * @param boolean
     * @throws IllegalArgumentException
     */
    public static function setRewrite($mode)
    {
        if (!is_bool($mode))
            throw new IllegalArgumentException();
        self::$rewrite = $mode;
    }
    public static function getRewrite()
    {
        return self::$rewrite;
    }
    public function getImageInfo()
    {
        if (!isset($this->imageInfo))
            $this->imageInfo = @getimagesize($this->getFilePath());
        return $this->imageInfo;
    }
    /**
     * Возвращает две первые цифры
     * версии php, разделённые точкой.
     * Например: 5.2, 5.3
     *
     * @since 2.0.1
     *
     * @return string
     */
    public static function getShortPHPVersion()
    {
        $matches = array();
        preg_match("@^d.d@", phpversion(), $matches);
        return $matches[0];
    }
    public static function isSupportedGD()
    {
        return function_exists('gd_info');
    }
    /**
     *
     * Возвращает результат работы функции gd_info()
     * или false если библиотека gd не доступна
     *
     * @return array|bool
     */
    public static function getGDinfo()
    {
        if (!self::isSupportedGD())
            return false;
        if (!isset(self::$gdInfo))
            self::$gdInfo = gd_info();
        return self::$gdInfo;
    }
    public function getFilePath()
    {
        return $this->filePath;
    }
    private function setSize(Size $s)
    {
        $this->size = $s;
    }
    public function getSourceImage()
    {
        return $this->sourceImage;
    }
    public function getMimeType()
    {
        $imageInfo = $this->getImageInfo();
        return $imageInfo['mime'];
    }
    public function getSize()
    {
        return clone $this->size;
    }
    public function getWidth()
    {
        return $this->getSize()->getWidth();
    }
    public function getHeight()
    {
        return $this->getSize()->getHeight();
    }
    public function getResource()
    {
        return $this->resource;
    }
    /**
     * @ignore
     */
    protected function setResource($resource)
    {
        return $this->resource = $resource;
    }
    // static getters and setters
    /**
     *
     * @param int качество изображения от 0 до 100
     * @throws IllegalArgumentException
     */
    public static function setQuality($q)
    {
        $q = (int)$q;
        if (!is_integer($q) || $q <= 0 || $q > 100)
            throw new IllegalArgumentException();
        self::$quality = $q;
    }
    public static function getQuality()
    {
        return self::$quality;
    }
    /**
     *
     * @param boolean
     * @throws IllegalArgumentException
     */
    public static function setTransparency($mode)
    {
        if(!is_bool($mode))
            throw new IllegalArgumentException();
        self::$transparency = $mode;
    }
    public static function getTransparency()
    {
        return self::$transparency;
    }
    public static function setBackgroundColor() // $color || $r, $r, $b || $code
    {
        $a = func_get_args();
        if (count($a) == 1)
        {
            if ($a[0] instanceof AcColor)
            {
                self::$backgroundColor = $a[0];
            }
            else
            {
                self::$backgroundColor = new AcColor($a[0]);
            }
        }
        else if (count($a) == 3)
        {
            self::$backgroundColor = new AcColor($a[0], $a[1], $a[2]);
        }
        else
        {
            throw new IllegalArgumentException();
        }
    }
    public static function getBackgroundColor()
    {
        if (is_integer(self::$backgroundColor))
            self::$backgroundColor = new AcColor(self::$backgroundColor);
        return self::$backgroundColor;
    }
    public function getCornerLogo()
    {
        return self::$cornerLogo;
    }
    /**
     *
     * @param int номер угла изображения<br />
     * 0 1<br />
     * 2 3
     *
     * @see AcImage::TOP_LEFT
     * @see AcImage::TOP_RIGHT
     * @see AcImage::BOTTOM_RIGHT
     * @see AcImage::BOTTOM_LEFT
     *
     * @throws IllegalArgumentException
     */
    public function setCornerLogo($corner)
    {
        if(!self::isCorrectCorner($corner))
            throw new IllegalArgumentException();
        self::$cornerLogo = $corner;
    }
    /**
     *
     * @param float от 0 <= $maxPropotionsLogo < 1
     * @throws IllegalArgumentException
     */
    public static function setMaxProportionLogo($maxPropotionsLogo)
    {
        if (!is_float($maxPropotionsLogo) || $maxPropotionsLogo > 1 ||
                        $maxPropotionsLogo <= 0)
            throw new IllegalArgumentException();
        self::$maxProportionLogo = $maxPropotionsLogo;
    }
    public static function getMaxProportionLogo()
    {
        return self::$maxProportionLogo;
    }
    /**
     *
     * @param float от 0 <= $paddingProportionLogo < 1
     * @throws IllegalArgumentException
     */
    public static function setPaddingProportionLogo($paddingProportionLogo)
    {
        if (!is_float($paddingProportionLogo) || $paddingProportionLogo > 1 ||
                        $paddingProportionLogo <= 0)
            throw new IllegalArgumentException();
        self::$paddingProportionLogo = $paddingProportionLogo;
    }
    public static function getPaddingProportionLogo()
    {
            return self::$paddingProportionLogo;
    }
}
?>