xref: /dokuwiki/vendor/splitbrain/slika/src/GdAdapter.php (revision dd9e8e5ea54469964faab99223a61bd48146ac42)
192a8473aSAndreas Gohr<?php /** @noinspection PhpComposerExtensionStubsInspection */
292a8473aSAndreas Gohr
392a8473aSAndreas Gohr
492a8473aSAndreas Gohrnamespace splitbrain\slika;
592a8473aSAndreas Gohr
65c25a071SAndreas Gohr/**
75c25a071SAndreas Gohr * Image processing adapter for PHP's libGD
85c25a071SAndreas Gohr */
992a8473aSAndreas Gohrclass GdAdapter extends Adapter
1092a8473aSAndreas Gohr{
1192a8473aSAndreas Gohr    /** @var resource libGD image */
1292a8473aSAndreas Gohr    protected $image;
1392a8473aSAndreas Gohr    /** @var int width of the current image */
1492a8473aSAndreas Gohr    protected $width = 0;
1592a8473aSAndreas Gohr    /** @var int height of the current image */
1692a8473aSAndreas Gohr    protected $height = 0;
1792a8473aSAndreas Gohr    /** @var string the extension of the file we're working with */
1892a8473aSAndreas Gohr    protected $extension;
1992a8473aSAndreas Gohr
2092a8473aSAndreas Gohr
2192a8473aSAndreas Gohr    /** @inheritDoc */
2292a8473aSAndreas Gohr    public function __construct($imagepath, $options = [])
2392a8473aSAndreas Gohr    {
2492a8473aSAndreas Gohr        parent::__construct($imagepath, $options);
2592a8473aSAndreas Gohr        $this->image = $this->loadImage($imagepath);
2692a8473aSAndreas Gohr    }
2792a8473aSAndreas Gohr
2892a8473aSAndreas Gohr    /**
2992a8473aSAndreas Gohr     * Clean up
3092a8473aSAndreas Gohr     */
3192a8473aSAndreas Gohr    public function __destruct()
3292a8473aSAndreas Gohr    {
3362c98cd6SAndreas Gohr        // destroy the GD image resource (only needed on PHP < 8.0)
3492a8473aSAndreas Gohr        if (is_resource($this->image)) {
3592a8473aSAndreas Gohr            imagedestroy($this->image);
3692a8473aSAndreas Gohr        }
3792a8473aSAndreas Gohr    }
3892a8473aSAndreas Gohr
3992a8473aSAndreas Gohr    /** @inheritDoc
4092a8473aSAndreas Gohr     * @throws Exception
4192a8473aSAndreas Gohr     */
4292a8473aSAndreas Gohr    public function autorotate()
4392a8473aSAndreas Gohr    {
4492a8473aSAndreas Gohr        if ($this->extension !== 'jpeg') {
4592a8473aSAndreas Gohr            return $this;
4692a8473aSAndreas Gohr        }
47*dd9e8e5eSAndreas Gohr        return $this->rotate(ImageInfo::readExifOrientation($this->imagepath));
4892a8473aSAndreas Gohr    }
4992a8473aSAndreas Gohr
5092a8473aSAndreas Gohr    /**
5192a8473aSAndreas Gohr     * @inheritDoc
5292a8473aSAndreas Gohr     * @throws Exception
5392a8473aSAndreas Gohr     */
5492a8473aSAndreas Gohr    public function rotate($orientation)
5592a8473aSAndreas Gohr    {
5692a8473aSAndreas Gohr        $orientation = (int)$orientation;
5792a8473aSAndreas Gohr        if ($orientation < 0 || $orientation > 8) {
5892a8473aSAndreas Gohr            throw new Exception('Unknown rotation given');
5992a8473aSAndreas Gohr        }
6092a8473aSAndreas Gohr
6192a8473aSAndreas Gohr        if ($orientation <= 1) {
6292a8473aSAndreas Gohr            // no rotation wanted
6392a8473aSAndreas Gohr            return $this;
6492a8473aSAndreas Gohr        }
6592a8473aSAndreas Gohr
6692a8473aSAndreas Gohr        // fill color
6792a8473aSAndreas Gohr        $transparency = imagecolorallocatealpha($this->image, 0, 0, 0, 127);
6892a8473aSAndreas Gohr
69*dd9e8e5eSAndreas Gohr        // rotate (orientation 2 is a flip-only case and keeps $this->image)
70*dd9e8e5eSAndreas Gohr        $image = $this->image;
7192a8473aSAndreas Gohr        if (in_array($orientation, [3, 4])) {
72c13ef3baSAndreas Gohr            $image = imagerotate($this->image, 180, $transparency);
73*dd9e8e5eSAndreas Gohr        } elseif (in_array($orientation, [5, 6])) {
74c13ef3baSAndreas Gohr            $image = imagerotate($this->image, -90, $transparency);
7592a8473aSAndreas Gohr            list($this->width, $this->height) = [$this->height, $this->width];
7692a8473aSAndreas Gohr        } elseif (in_array($orientation, [7, 8])) {
77c13ef3baSAndreas Gohr            $image = imagerotate($this->image, 90, $transparency);
7892a8473aSAndreas Gohr            list($this->width, $this->height) = [$this->height, $this->width];
7992a8473aSAndreas Gohr        }
8092a8473aSAndreas Gohr
8192a8473aSAndreas Gohr        // additionally flip
8292a8473aSAndreas Gohr        if (in_array($orientation, [2, 5, 7, 4])) {
8392a8473aSAndreas Gohr            imageflip($image, IMG_FLIP_HORIZONTAL);
8492a8473aSAndreas Gohr        }
8592a8473aSAndreas Gohr
86*dd9e8e5eSAndreas Gohr        if ($image !== $this->image) {
8762c98cd6SAndreas Gohr            $this->__destruct(); // destroy old image
8892a8473aSAndreas Gohr            $this->image = $image;
89*dd9e8e5eSAndreas Gohr        }
9092a8473aSAndreas Gohr
9192a8473aSAndreas Gohr        //keep png alpha channel if possible
9292a8473aSAndreas Gohr        if ($this->extension == 'png' && function_exists('imagesavealpha')) {
9392a8473aSAndreas Gohr            imagealphablending($this->image, false);
9492a8473aSAndreas Gohr            imagesavealpha($this->image, true);
9592a8473aSAndreas Gohr        }
9692a8473aSAndreas Gohr
9792a8473aSAndreas Gohr        return $this;
9892a8473aSAndreas Gohr    }
9992a8473aSAndreas Gohr
10092a8473aSAndreas Gohr    /**
10192a8473aSAndreas Gohr     * @inheritDoc
10292a8473aSAndreas Gohr     * @throws Exception
10392a8473aSAndreas Gohr     */
10492a8473aSAndreas Gohr    public function resize($width, $height)
10592a8473aSAndreas Gohr    {
106*dd9e8e5eSAndreas Gohr        list($width, $height) = ImageInfo::boundingBox($this->width, $this->height, $width, $height);
10792a8473aSAndreas Gohr        $this->resizeOperation($width, $height);
10892a8473aSAndreas Gohr        return $this;
10992a8473aSAndreas Gohr    }
11092a8473aSAndreas Gohr
11192a8473aSAndreas Gohr    /**
11292a8473aSAndreas Gohr     * @inheritDoc
11392a8473aSAndreas Gohr     * @throws Exception
11492a8473aSAndreas Gohr     */
11592a8473aSAndreas Gohr    public function crop($width, $height)
11692a8473aSAndreas Gohr    {
11792a8473aSAndreas Gohr        list($this->width, $this->height, $offsetX, $offsetY) = $this->cropPosition($width, $height);
11892a8473aSAndreas Gohr        $this->resizeOperation($width, $height, $offsetX, $offsetY);
11992a8473aSAndreas Gohr        return $this;
12092a8473aSAndreas Gohr    }
12192a8473aSAndreas Gohr
12292a8473aSAndreas Gohr    /**
12392a8473aSAndreas Gohr     * @inheritDoc
12492a8473aSAndreas Gohr     * @throws Exception
12592a8473aSAndreas Gohr     */
12692a8473aSAndreas Gohr    public function save($path, $extension = '')
12792a8473aSAndreas Gohr    {
12892a8473aSAndreas Gohr        if ($extension === 'jpg') {
12992a8473aSAndreas Gohr            $extension = 'jpeg';
13092a8473aSAndreas Gohr        }
13192a8473aSAndreas Gohr        if ($extension === '') {
13292a8473aSAndreas Gohr            $extension = $this->extension;
13392a8473aSAndreas Gohr        }
13492a8473aSAndreas Gohr        $saver = 'image' . $extension;
13592a8473aSAndreas Gohr        if (!function_exists($saver)) {
13692a8473aSAndreas Gohr            throw new Exception('Can not save image format ' . $extension);
13792a8473aSAndreas Gohr        }
13892a8473aSAndreas Gohr
13992a8473aSAndreas Gohr        if ($extension == 'jpeg') {
14092a8473aSAndreas Gohr            imagejpeg($this->image, $path, $this->options['quality']);
14192a8473aSAndreas Gohr        } else {
14292a8473aSAndreas Gohr            $saver($this->image, $path);
14392a8473aSAndreas Gohr        }
14492a8473aSAndreas Gohr
14562c98cd6SAndreas Gohr        $this->__destruct();
14692a8473aSAndreas Gohr    }
14792a8473aSAndreas Gohr
14892a8473aSAndreas Gohr    /**
14992a8473aSAndreas Gohr     * Initialize libGD on the given image
15092a8473aSAndreas Gohr     *
15192a8473aSAndreas Gohr     * @param string $path
15292a8473aSAndreas Gohr     * @return resource
15392a8473aSAndreas Gohr     * @throws Exception
15492a8473aSAndreas Gohr     */
15592a8473aSAndreas Gohr    protected function loadImage($path)
15692a8473aSAndreas Gohr    {
15792a8473aSAndreas Gohr        // Figure out the file info
15892a8473aSAndreas Gohr        $info = getimagesize($path);
15992a8473aSAndreas Gohr        if ($info === false) {
16092a8473aSAndreas Gohr            throw new Exception('Failed to read image information');
16192a8473aSAndreas Gohr        }
16292a8473aSAndreas Gohr        $this->width = $info[0];
16392a8473aSAndreas Gohr        $this->height = $info[1];
16492a8473aSAndreas Gohr
16592a8473aSAndreas Gohr        // what type of image is it?
16692a8473aSAndreas Gohr        $this->extension = image_type_to_extension($info[2], false);
16792a8473aSAndreas Gohr        $creator = 'imagecreatefrom' . $this->extension;
16892a8473aSAndreas Gohr        if (!function_exists($creator)) {
16992a8473aSAndreas Gohr            throw new Exception('Can not work with image format ' . $this->extension);
17092a8473aSAndreas Gohr        }
17192a8473aSAndreas Gohr
17292a8473aSAndreas Gohr        // create the GD instance
17392a8473aSAndreas Gohr        $image = @$creator($path);
17492a8473aSAndreas Gohr
17592a8473aSAndreas Gohr        if ($image === false) {
17692a8473aSAndreas Gohr            throw new Exception('Failed to load image wiht libGD');
17792a8473aSAndreas Gohr        }
17892a8473aSAndreas Gohr
17992a8473aSAndreas Gohr        return $image;
18092a8473aSAndreas Gohr    }
18192a8473aSAndreas Gohr
18292a8473aSAndreas Gohr    /**
18392a8473aSAndreas Gohr     * Creates a new blank image to which we can copy
18492a8473aSAndreas Gohr     *
18592a8473aSAndreas Gohr     * Tries to set up alpha/transparency stuff correctly
18692a8473aSAndreas Gohr     *
18792a8473aSAndreas Gohr     * @param int $width
18892a8473aSAndreas Gohr     * @param int $height
18992a8473aSAndreas Gohr     * @return resource
19092a8473aSAndreas Gohr     * @throws Exception
19192a8473aSAndreas Gohr     */
19292a8473aSAndreas Gohr    protected function createImage($width, $height)
19392a8473aSAndreas Gohr    {
19492a8473aSAndreas Gohr        // create a canvas to copy to, use truecolor if possible (except for gif)
19592a8473aSAndreas Gohr        $canvas = false;
19692a8473aSAndreas Gohr        if (function_exists('imagecreatetruecolor') && $this->extension != 'gif') {
19792a8473aSAndreas Gohr            $canvas = @imagecreatetruecolor($width, $height);
19892a8473aSAndreas Gohr        }
19992a8473aSAndreas Gohr        if (!$canvas) {
20092a8473aSAndreas Gohr            $canvas = @imagecreate($width, $height);
20192a8473aSAndreas Gohr        }
20292a8473aSAndreas Gohr        if (!$canvas) {
20392a8473aSAndreas Gohr            throw new Exception('Failed to create new canvas');
20492a8473aSAndreas Gohr        }
20592a8473aSAndreas Gohr
20692a8473aSAndreas Gohr        //keep png alpha channel if possible
20792a8473aSAndreas Gohr        if ($this->extension == 'png' && function_exists('imagesavealpha')) {
20892a8473aSAndreas Gohr            imagealphablending($canvas, false);
20992a8473aSAndreas Gohr            imagesavealpha($canvas, true);
21092a8473aSAndreas Gohr        }
21192a8473aSAndreas Gohr
21292a8473aSAndreas Gohr        //keep gif transparent color if possible
2132cadabe7SAndreas Gohr        if ($this->extension == 'gif') {
2142cadabe7SAndreas Gohr            $this->keepGifTransparency($this->image, $canvas);
2152cadabe7SAndreas Gohr        }
2162cadabe7SAndreas Gohr
2172cadabe7SAndreas Gohr        return $canvas;
2182cadabe7SAndreas Gohr    }
2192cadabe7SAndreas Gohr
2202cadabe7SAndreas Gohr    /**
2212cadabe7SAndreas Gohr     * Copy transparency from gif to gif
2222cadabe7SAndreas Gohr     *
2232cadabe7SAndreas Gohr     * If no transparency is found or the PHP does not support it, the canvas is filled with white
2242cadabe7SAndreas Gohr     *
2252cadabe7SAndreas Gohr     * @param resource $image Original image
2262cadabe7SAndreas Gohr     * @param resource $canvas New, empty image
2272cadabe7SAndreas Gohr     * @return void
2282cadabe7SAndreas Gohr     */
2292cadabe7SAndreas Gohr    protected function keepGifTransparency($image, $canvas)
2302cadabe7SAndreas Gohr    {
2312cadabe7SAndreas Gohr        if (!function_exists('imagefill') || !function_exists('imagecolorallocate')) {
2322cadabe7SAndreas Gohr            return;
2332cadabe7SAndreas Gohr        }
2342cadabe7SAndreas Gohr
2352cadabe7SAndreas Gohr        try {
2362cadabe7SAndreas Gohr            if (!function_exists('imagecolorsforindex') || !function_exists('imagecolortransparent')) {
2372cadabe7SAndreas Gohr                throw new \Exception('missing alpha methods');
2382cadabe7SAndreas Gohr            }
2392cadabe7SAndreas Gohr
2402cadabe7SAndreas Gohr            $transcolorindex = @imagecolortransparent($image);
2412cadabe7SAndreas Gohr            $transcolor = @imagecolorsforindex($image, $transcolorindex);
2422cadabe7SAndreas Gohr            if (!$transcolor) {
2432cadabe7SAndreas Gohr                // pre-PHP8 false is returned, in PHP8 an exception is thrown
2442cadabe7SAndreas Gohr                throw new \ValueError('no valid alpha color');
2452cadabe7SAndreas Gohr            }
2462cadabe7SAndreas Gohr
24792a8473aSAndreas Gohr            $transcolorindex = @imagecolorallocate(
24892a8473aSAndreas Gohr                $canvas,
24992a8473aSAndreas Gohr                $transcolor['red'],
25092a8473aSAndreas Gohr                $transcolor['green'],
25192a8473aSAndreas Gohr                $transcolor['blue']
25292a8473aSAndreas Gohr            );
25392a8473aSAndreas Gohr            @imagefill($canvas, 0, 0, $transcolorindex);
25492a8473aSAndreas Gohr            @imagecolortransparent($canvas, $transcolorindex);
25592a8473aSAndreas Gohr
2562cadabe7SAndreas Gohr        } catch (\Throwable $ignored) {
2572cadabe7SAndreas Gohr            //filling with white
2582cadabe7SAndreas Gohr            $whitecolorindex = @imagecolorallocate($canvas, 255, 255, 255);
2592cadabe7SAndreas Gohr            @imagefill($canvas, 0, 0, $whitecolorindex);
2602cadabe7SAndreas Gohr        }
26192a8473aSAndreas Gohr    }
26292a8473aSAndreas Gohr
26392a8473aSAndreas Gohr    /**
26492a8473aSAndreas Gohr     * Calculates crop position
26592a8473aSAndreas Gohr     *
26692a8473aSAndreas Gohr     * Given the wanted final size, this calculates which exact area needs to be cut
26792a8473aSAndreas Gohr     * from the original image to be then resized to the wanted dimensions.
26892a8473aSAndreas Gohr     *
26992a8473aSAndreas Gohr     * @param int $width
27092a8473aSAndreas Gohr     * @param int $height
27192a8473aSAndreas Gohr     * @return array (cropWidth, cropHeight, offsetX, offsetY)
27292a8473aSAndreas Gohr     * @throws Exception
27392a8473aSAndreas Gohr     */
27492a8473aSAndreas Gohr    protected function cropPosition($width, $height)
27592a8473aSAndreas Gohr    {
27692a8473aSAndreas Gohr        if ($width == 0 && $height == 0) {
27792a8473aSAndreas Gohr            throw new Exception('You can not crop to 0x0');
27892a8473aSAndreas Gohr        }
27992a8473aSAndreas Gohr
28092a8473aSAndreas Gohr        if (!$height) {
28192a8473aSAndreas Gohr            $height = $width;
28292a8473aSAndreas Gohr        }
28392a8473aSAndreas Gohr
28492a8473aSAndreas Gohr        if (!$width) {
28592a8473aSAndreas Gohr            $width = $height;
28692a8473aSAndreas Gohr        }
28792a8473aSAndreas Gohr
28892a8473aSAndreas Gohr        // calculate ratios
28992a8473aSAndreas Gohr        $oldRatio = $this->width / $this->height;
29092a8473aSAndreas Gohr        $newRatio = $width / $height;
29192a8473aSAndreas Gohr
29292a8473aSAndreas Gohr        // calulate new size
29392a8473aSAndreas Gohr        if ($newRatio >= 1) {
29492a8473aSAndreas Gohr            if ($newRatio > $oldRatio) {
29592a8473aSAndreas Gohr                $cropWidth = $this->width;
29692a8473aSAndreas Gohr                $cropHeight = (int)($this->width / $newRatio);
29792a8473aSAndreas Gohr            } else {
29892a8473aSAndreas Gohr                $cropWidth = (int)($this->height * $newRatio);
29992a8473aSAndreas Gohr                $cropHeight = $this->height;
30092a8473aSAndreas Gohr            }
30192a8473aSAndreas Gohr        } else {
30292a8473aSAndreas Gohr            if ($newRatio < $oldRatio) {
30392a8473aSAndreas Gohr                $cropWidth = (int)($this->height * $newRatio);
30492a8473aSAndreas Gohr                $cropHeight = $this->height;
30592a8473aSAndreas Gohr            } else {
30692a8473aSAndreas Gohr                $cropWidth = $this->width;
30792a8473aSAndreas Gohr                $cropHeight = (int)($this->width / $newRatio);
30892a8473aSAndreas Gohr            }
30992a8473aSAndreas Gohr        }
31092a8473aSAndreas Gohr
31192a8473aSAndreas Gohr        // calculate crop offset
31292a8473aSAndreas Gohr        $offsetX = (int)(($this->width - $cropWidth) / 2);
31392a8473aSAndreas Gohr        $offsetY = (int)(($this->height - $cropHeight) / 2);
31492a8473aSAndreas Gohr
31592a8473aSAndreas Gohr        return [$cropWidth, $cropHeight, $offsetX, $offsetY];
31692a8473aSAndreas Gohr    }
31792a8473aSAndreas Gohr
31892a8473aSAndreas Gohr    /**
31992a8473aSAndreas Gohr     * resize or crop images using PHP's libGD support
32092a8473aSAndreas Gohr     *
32192a8473aSAndreas Gohr     * @param int $toWidth desired width
32292a8473aSAndreas Gohr     * @param int $toHeight desired height
32392a8473aSAndreas Gohr     * @param int $offsetX offset of crop centre
32492a8473aSAndreas Gohr     * @param int $offsetY offset of crop centre
32592a8473aSAndreas Gohr     * @throws Exception
32692a8473aSAndreas Gohr     */
32792a8473aSAndreas Gohr    protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0)
32892a8473aSAndreas Gohr    {
32992a8473aSAndreas Gohr        $newimg = $this->createImage($toWidth, $toHeight);
33092a8473aSAndreas Gohr
33192a8473aSAndreas Gohr        //try resampling first, fall back to resizing
33292a8473aSAndreas Gohr        if (
33392a8473aSAndreas Gohr            !function_exists('imagecopyresampled') ||
33492a8473aSAndreas Gohr            !@imagecopyresampled(
33592a8473aSAndreas Gohr                $newimg,
33692a8473aSAndreas Gohr                $this->image,
33792a8473aSAndreas Gohr                0,
33892a8473aSAndreas Gohr                0,
33992a8473aSAndreas Gohr                $offsetX,
34092a8473aSAndreas Gohr                $offsetY,
34192a8473aSAndreas Gohr                $toWidth,
34292a8473aSAndreas Gohr                $toHeight,
34392a8473aSAndreas Gohr                $this->width,
34492a8473aSAndreas Gohr                $this->height
34592a8473aSAndreas Gohr            )
34692a8473aSAndreas Gohr        ) {
34792a8473aSAndreas Gohr            imagecopyresized(
34892a8473aSAndreas Gohr                $newimg,
34992a8473aSAndreas Gohr                $this->image,
35092a8473aSAndreas Gohr                0,
35192a8473aSAndreas Gohr                0,
35292a8473aSAndreas Gohr                $offsetX,
35392a8473aSAndreas Gohr                $offsetY,
35492a8473aSAndreas Gohr                $toWidth,
35592a8473aSAndreas Gohr                $toHeight,
35692a8473aSAndreas Gohr                $this->width,
35792a8473aSAndreas Gohr                $this->height
35892a8473aSAndreas Gohr            );
35992a8473aSAndreas Gohr        }
36092a8473aSAndreas Gohr
36192a8473aSAndreas Gohr        // destroy original GD image ressource and replace with new one
36262c98cd6SAndreas Gohr        $this->__destruct();
36392a8473aSAndreas Gohr        $this->image = $newimg;
36492a8473aSAndreas Gohr        $this->width = $toWidth;
36592a8473aSAndreas Gohr        $this->height = $toHeight;
36692a8473aSAndreas Gohr    }
36792a8473aSAndreas Gohr
36892a8473aSAndreas Gohr}
369