ptejada/darkroom

View on GitHub
src/Tool/Resize.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

namespace Darkroom\Tool;

use Darkroom\Editor;
use Darkroom\Image;
use Darkroom\ImageResource;
use Darkroom\Utility\Color;

/**
 * Class Resize resides an image
 *
 * @package Darkroom\Tool
 */
class Resize extends AbstractTool
{
    const MODE_COLOR_FILL       = 1;
    const MODE_IMAGE_FILL       = 2;
    const MODE_TRANSPARENT_FILL = 16;
    const MODE_FILL             = 19;
    const MODE_DISTORT          = 4;
    const MODE_RATIO            = 8;

    /** @var int The resize mode */
    protected $mode = self::MODE_RATIO;
    /** @var int New image height in pixels */
    protected $height;
    /** @var int New image width in pixels */
    protected $width;
    /** @var int The decimal percent to resize the image by */
    protected $decimalPercent = 1;
    /** @var Color */
    protected $color;
    /** @var Image */
    protected $fillImage;

    /**
     * Set the dimensions of the new image
     *
     * @param int $width  New width in pixels
     * @param int $height New height in pixels
     *
     * @return Resize
     */
    public function to($width, $height = 0)
    {
        $this->width  = (int) $width;
        $this->height = (int) $height;

        $this->mode = ($this->width xor $this->height) ? self::MODE_RATIO : self::MODE_COLOR_FILL;

        return $this;
    }

    /**
     * Set the height of the new image
     *
     * @param int $height New height in pixels
     *
     * @return Resize
     */
    public function heightTo($height)
    {
        $this->mode = self::MODE_RATIO;

        $this->height = $height;
        return $this;
    }

    /**
     * Set the dimensions of the new image by a decimal percentage
     * Ex: 0.5 is 50%, 1.15 is 115%
     *
     * @param float $percent The percentage in decimal
     *
     * @return $this
     */
    public function by($percent)
    {
        if ($percent > 1) {
            $this->distort();
        } else {
            $this->mode = self::MODE_RATIO;
        }

        $this->decimalPercent = $percent;
        return $this;
    }

    /**
     * Keep original aspect ratio and use an image as the background
     *
     * @param Image $image The image to use as the background
     *
     * @return Resize
     */
    public function withImageFill(Image $image)
    {
        $this->mode      = self::MODE_IMAGE_FILL;
        $this->fillImage = $image;
        return $this;
    }

    /**
     * Keep original aspect ratio and use a color as the background
     *
     * @param string|int[]|Color $color The background color in Hex, RGB, HTML named colors or Color object
     *
     * @return Resize
     */
    public function withColorFill($color)
    {
        $this->mode  = self::MODE_COLOR_FILL;
        $this->color = ($color instanceof Color) ? $color : new Color($color);
        return $this;
    }

    /**
     * Makes the background fill color transparent
     */
    public function withTransparentFill()
    {
        $this->mode = self::MODE_TRANSPARENT_FILL;

        return $this;
    }

    /**
     * Ignore aspect ratio and distort the image if necessary
     *
     * @return Resize
     */
    public function distort()
    {
        if ($this->isMode(self::MODE_FILL)) {
            $this->mode = $this->mode | self::MODE_DISTORT;
        } else {
            $this->mode = self::MODE_DISTORT;
        }
        return $this;
    }

    /**
     * @inheritdoc
     */
    protected function execute()
    {
        $image = $this->editor->image();

        if (!$this->height && !$this->width && $this->decimalPercent) {
            $this->height = $image->height() * $this->decimalPercent;
            $this->width  = $image->width() * $this->decimalPercent;
        }

        $source_x = $source_y = 0;
        $target_x = $target_y = 0;

        $source_width  = $image->width();
        $source_height = $image->height();

        $newWidth = $newHeight = 0;

        if ($this->isMode(self::MODE_RATIO | self::MODE_DISTORT)) {
            list($newWidth, $newHeight) = $this->newAspectSize();
        }

        if ($this->isMode(self::MODE_FILL)) {
            $newWidth  = $this->width ?: ($this->height * $source_width) / $source_height;
            $newHeight = $this->height ?: ($this->width * $source_height) / $source_width;

            list($sample_width, $sample_height) = $this->newAspectSize();

            // Center the sample in the new image
            $target_x = abs($sample_width - $newWidth) / 2;
            $target_y = abs($sample_height - $newHeight) / 2;
        }

        if (empty($sample_width)) {
            $sample_width = $newWidth;
        }

        if (empty($sample_height)) {
            $sample_height = $newHeight;
        }

        if ($newHeight != $source_height && $newWidth != $source_width) {
            $newImage = $this->background($newWidth, $newHeight);
            imagecopyresampled($newImage->resource(), $image->resource(), $target_x, $target_y, $source_x, $source_y,
                $sample_width, $sample_height, $source_width, $source_height
            );

            return $newImage;
        }

        return null;
    }

    /**
     * Check if mode is enabled
     *
     * @param int $mode Mode to check if is enabled
     *
     * @return bool
     */
    protected function isMode($mode)
    {
        return ($this->mode & $mode) > 0;
    }

    /**
     * Generate background image
     *
     * @param int $width  Background width
     * @param int $height Background height
     *
     * @return ImageResource
     */
    protected function background($width, $height)
    {
        $image = Editor::canvas($width, $height);

        if ($this->isMode(self::MODE_COLOR_FILL) && $this->color) {
            list($red, $green, $blue) = $this->color->rgb();
            $customColor = imagecolorallocate($image->resource(), $red, $green, $blue);
            imagefilledrectangle($image->resource(), 0, 0, $width, $height, $customColor);
        }

        if ($this->isMode(self::MODE_TRANSPARENT_FILL)) {
            // TODO: Make the transparency work with the GIF images
            $transparent = imagecolorallocatealpha($image->resource(), 0, 0, 0, 127);
            imagealphablending($image->resource(), false);
            imagesavealpha($image->resource(), true);

            imagefilledrectangle($image->resource(), 0, 0, $width, $height, $transparent);
            imagecolortransparent($image->resource(), $transparent);

            // Convert image to PNG
            $this->editor->image()->convertTo(IMAGETYPE_PNG);
        }

        if ($this->isMode(self::MODE_IMAGE_FILL) && $this->fillImage) {
            $this->fillImage->edit()->resize()->to($width, $height)->distort()->apply();
            // TODO: Check if we must destroy the image first
            $image = $this->fillImage;
        }

        return $image;
    }

    /**
     * Calculate the dimensions of the new image sample while respecting the canvas size and original aspect ratio
     *
     * @return int[]
     */
    protected function newAspectSize()
    {
        $image = $this->editor->image();

        $max_width  = $this->width;
        $max_height = $this->height;

        $original_width  = $image->width();
        $original_height = $image->height();

        if ($this->isMode(self::MODE_DISTORT)) {
            $new_width  = $max_width;
            $new_height = $max_height;
        } else {
            $new_width  = $original_width > $max_width ? $max_width : $original_width;
            $new_height = $original_height > $max_height ? $max_height : $original_width;
        }

        if ($new_width xor $new_height) {
            $new_width  = $new_width ?: ($new_height * $original_width) / $original_height;
            $new_height = $new_height ?: ($new_width * $original_height) / $original_width;
        } else {
            if (($new_width / $new_height) !== ($original_width / $original_height)) {
                if (!$this->isMode(self::MODE_DISTORT)) {
                    if ($original_width > $new_width || $original_height > $new_height) {
                        if ($original_width > $new_width) {
                            $new_height = ($original_height / $original_width) * $new_height;
                        } else {
                            if ($original_height > $new_height) {
                                $new_width = ($original_width / $original_height) * $new_width;
                            }
                        }
                    }
                }
            }
        }

        return [round($new_width), round($new_height)];
    }
}