weserv/images

View on GitHub
src/Manipulators/Crop.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Weserv\Images\Manipulators;

use Jcupitt\Vips\Image;
use Jcupitt\Vips\Interesting;

/**
 * @property string $t
 * @property int $w
 * @property int $h
 * @property string $a
 * @property string $crop
 */
class Crop extends BaseManipulator
{
    /**
     * Perform crop image manipulation.
     *
     * @param Image $image The source image.
     *
     * @throws \Jcupitt\Vips\Exception
     *
     * @return Image The manipulated image.
     */
    public function run(Image $image): Image
    {
        $width = $this->w;
        $height = $this->h;
        $imageWidth = $image->width;
        $imageHeight = $image->height;

        $isSmartCrop = $this->a === Interesting::ENTROPY || $this->a === Interesting::ATTENTION;
        $isCropNeeded = $this->t === 'square' || $this->t === 'squaredown' ||
            ($this->t !== null && strpos($this->t, 'crop') === 0);

        if (($imageWidth !== $width || $imageHeight !== $height) && $isCropNeeded) {
            $minWidth = min($imageWidth, $width);
            $minHeight = min($imageHeight, $height);

            if ($isSmartCrop) {
                // Need to copy to memory, we have to stay seq.
                $image = $image->copyMemory()->smartcrop($minWidth, $minHeight, ['interesting' => $this->a]);
            } else {
                [$offsetPercentageX, $offsetPercentageY] = $this->getCrop();
                $offsetX = (int)(($imageWidth - $width) * ($offsetPercentageX / 100));
                $offsetY = (int)(($imageHeight - $height) * ($offsetPercentageY / 100));

                [$left, $top] = $this->calculateCrop(
                    [$imageWidth, $imageHeight],
                    [$width, $height],
                    [$offsetX, $offsetY]
                );

                $image = $image->crop($left, $top, $minWidth, $minHeight);
            }

            // Update to actual image dimensions
            $imageWidth = $minWidth;
            $imageHeight = $minHeight;
        }

        $coordinates = $this->resolveCropCoordinates($imageWidth, $imageHeight);

        if ($coordinates !== null) {
            $coordinates = $this->limitToImageBoundaries($image, $coordinates);

            $image = $image->crop(
                $coordinates[2],
                $coordinates[3],
                $coordinates[0],
                $coordinates[1]
            );
        }

        return $image;
    }

    /**
     * Resolve crop coordinates.
     *
     * @param int $imageWidth
     * @param int $imageHeight
     *
     * @return int[]|null The resolved coordinates.
     *
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    public function resolveCropCoordinates(int $imageWidth, int $imageHeight): ?array
    {
        if (!isset($this->crop)) {
            return null;
        }

        $coordinates = explode(',', $this->crop);

        if (\count($coordinates) !== 4
            || (!is_numeric($coordinates[0]))
            || (!is_numeric($coordinates[1]))
            || (!is_numeric($coordinates[2]))
            || (!is_numeric($coordinates[3]))
            || ($coordinates[0] <= 0)
            || ($coordinates[1] <= 0)
            || ($coordinates[2] < 0)
            || ($coordinates[3] < 0)
            || ($coordinates[2] >= $imageWidth)
            || ($coordinates[3] >= $imageHeight)
        ) {
            return null;
        }

        return [
            (int)$coordinates[0],
            (int)$coordinates[1],
            (int)$coordinates[2],
            (int)$coordinates[3]
        ];
    }

    /**
     * Resolve crop.
     *
     * @return int[] The resolved crop.
     */
    public function getCrop(): array
    {
        $cropMethods = [
            'top-left' => [0, 0],
            't' => [50, 0], // Deprecated use top instead
            'top' => [50, 0],
            'top-right' => [100, 0],
            'l' => [0, 50], // Deprecated use left instead
            'left' => [0, 50],
            'center' => [50, 50],
            'r' => [0, 50], // Deprecated use right instead
            'right' => [100, 50],
            'bottom-left' => [0, 100],
            'b' => [50, 100], // Deprecated use bottom instead
            'bottom' => [50, 100],
            'bottom-right' => [100, 100]
        ];

        if (isset($cropMethods[$this->a])) {
            return $cropMethods[$this->a];
        }

        // Focal point
        if ($this->a !== null && strpos($this->a, 'crop-') === 0) {
            $matches = explode('-', substr($this->a, 5));
            if (!isset($matches[2]) && is_numeric($matches[0]) && is_numeric($matches[1])) {
                if ($matches[0] > 100 || $matches[1] > 100) {
                    return [50, 50];
                }

                return [
                    (int)$matches[0],
                    (int)$matches[1]
                ];
            }
        }

        return [50, 50];
    }

    /**
     * Calculate the (left, top) coordinates of the output image
     * within the input image, applying the given x and y offsets.
     *
     * @param int[] $inCoordinates The image width/height.
     * @param int[] $outCoordinates The output width/height.
     * @param int[] $offsets The x/y offset.
     *
     * @return int[] The crop offset.
     *
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    public function calculateCrop(array $inCoordinates, array $outCoordinates, array $offsets): array
    {
        // Default values
        $left = 0;
        $top = 0;

        // Assign only if valid
        if ($offsets[0] >= 0 && $offsets[0] < ($inCoordinates[0] - $outCoordinates[0])) {
            $left = $offsets[0];
        } elseif ($offsets[0] >= ($inCoordinates[0] - $outCoordinates[0])) {
            $left = $inCoordinates[0] - $outCoordinates[0];
        }

        if ($offsets[1] >= 0 && $offsets[1] < ($inCoordinates[1] - $outCoordinates[1])) {
            $top = $offsets[1];
        } elseif ($offsets[1] >= ($inCoordinates[1] - $outCoordinates[1])) {
            $top = $inCoordinates[1] - $outCoordinates[1];
        }

        // The resulting left and top could have been outside the image after calculation from bottom/right edges
        if ($left < 0) {
            $left = 0;
        }

        if ($top < 0) {
            $top = 0;
        }

        return [$left, $top];
    }

    /**
     * Limit coordinates to image boundaries.
     *
     * @param Image $image The source image.
     * @param int[] $coordinates The coordinates.
     *
     * @return int[] The limited coordinates.
     */
    public function limitToImageBoundaries(Image $image, array $coordinates): array
    {
        if ($coordinates[0] > ($image->width - $coordinates[2])) {
            $coordinates[0] = $image->width - $coordinates[2];
        }

        if ($coordinates[1] > ($image->height - $coordinates[3])) {
            $coordinates[1] = $image->height - $coordinates[3];
        }

        return $coordinates;
    }
}