xref: /dokuwiki/vendor/splitbrain/slika/src/GdAdapter.php (revision dd9e8e5ea54469964faab99223a61bd48146ac42)
1<?php /** @noinspection PhpComposerExtensionStubsInspection */
2
3
4namespace splitbrain\slika;
5
6/**
7 * Image processing adapter for PHP's libGD
8 */
9class GdAdapter extends Adapter
10{
11    /** @var resource libGD image */
12    protected $image;
13    /** @var int width of the current image */
14    protected $width = 0;
15    /** @var int height of the current image */
16    protected $height = 0;
17    /** @var string the extension of the file we're working with */
18    protected $extension;
19
20
21    /** @inheritDoc */
22    public function __construct($imagepath, $options = [])
23    {
24        parent::__construct($imagepath, $options);
25        $this->image = $this->loadImage($imagepath);
26    }
27
28    /**
29     * Clean up
30     */
31    public function __destruct()
32    {
33        // destroy the GD image resource (only needed on PHP < 8.0)
34        if (is_resource($this->image)) {
35            imagedestroy($this->image);
36        }
37    }
38
39    /** @inheritDoc
40     * @throws Exception
41     */
42    public function autorotate()
43    {
44        if ($this->extension !== 'jpeg') {
45            return $this;
46        }
47        return $this->rotate(ImageInfo::readExifOrientation($this->imagepath));
48    }
49
50    /**
51     * @inheritDoc
52     * @throws Exception
53     */
54    public function rotate($orientation)
55    {
56        $orientation = (int)$orientation;
57        if ($orientation < 0 || $orientation > 8) {
58            throw new Exception('Unknown rotation given');
59        }
60
61        if ($orientation <= 1) {
62            // no rotation wanted
63            return $this;
64        }
65
66        // fill color
67        $transparency = imagecolorallocatealpha($this->image, 0, 0, 0, 127);
68
69        // rotate (orientation 2 is a flip-only case and keeps $this->image)
70        $image = $this->image;
71        if (in_array($orientation, [3, 4])) {
72            $image = imagerotate($this->image, 180, $transparency);
73        } elseif (in_array($orientation, [5, 6])) {
74            $image = imagerotate($this->image, -90, $transparency);
75            list($this->width, $this->height) = [$this->height, $this->width];
76        } elseif (in_array($orientation, [7, 8])) {
77            $image = imagerotate($this->image, 90, $transparency);
78            list($this->width, $this->height) = [$this->height, $this->width];
79        }
80
81        // additionally flip
82        if (in_array($orientation, [2, 5, 7, 4])) {
83            imageflip($image, IMG_FLIP_HORIZONTAL);
84        }
85
86        if ($image !== $this->image) {
87            $this->__destruct(); // destroy old image
88            $this->image = $image;
89        }
90
91        //keep png alpha channel if possible
92        if ($this->extension == 'png' && function_exists('imagesavealpha')) {
93            imagealphablending($this->image, false);
94            imagesavealpha($this->image, true);
95        }
96
97        return $this;
98    }
99
100    /**
101     * @inheritDoc
102     * @throws Exception
103     */
104    public function resize($width, $height)
105    {
106        list($width, $height) = ImageInfo::boundingBox($this->width, $this->height, $width, $height);
107        $this->resizeOperation($width, $height);
108        return $this;
109    }
110
111    /**
112     * @inheritDoc
113     * @throws Exception
114     */
115    public function crop($width, $height)
116    {
117        list($this->width, $this->height, $offsetX, $offsetY) = $this->cropPosition($width, $height);
118        $this->resizeOperation($width, $height, $offsetX, $offsetY);
119        return $this;
120    }
121
122    /**
123     * @inheritDoc
124     * @throws Exception
125     */
126    public function save($path, $extension = '')
127    {
128        if ($extension === 'jpg') {
129            $extension = 'jpeg';
130        }
131        if ($extension === '') {
132            $extension = $this->extension;
133        }
134        $saver = 'image' . $extension;
135        if (!function_exists($saver)) {
136            throw new Exception('Can not save image format ' . $extension);
137        }
138
139        if ($extension == 'jpeg') {
140            imagejpeg($this->image, $path, $this->options['quality']);
141        } else {
142            $saver($this->image, $path);
143        }
144
145        $this->__destruct();
146    }
147
148    /**
149     * Initialize libGD on the given image
150     *
151     * @param string $path
152     * @return resource
153     * @throws Exception
154     */
155    protected function loadImage($path)
156    {
157        // Figure out the file info
158        $info = getimagesize($path);
159        if ($info === false) {
160            throw new Exception('Failed to read image information');
161        }
162        $this->width = $info[0];
163        $this->height = $info[1];
164
165        // what type of image is it?
166        $this->extension = image_type_to_extension($info[2], false);
167        $creator = 'imagecreatefrom' . $this->extension;
168        if (!function_exists($creator)) {
169            throw new Exception('Can not work with image format ' . $this->extension);
170        }
171
172        // create the GD instance
173        $image = @$creator($path);
174
175        if ($image === false) {
176            throw new Exception('Failed to load image wiht libGD');
177        }
178
179        return $image;
180    }
181
182    /**
183     * Creates a new blank image to which we can copy
184     *
185     * Tries to set up alpha/transparency stuff correctly
186     *
187     * @param int $width
188     * @param int $height
189     * @return resource
190     * @throws Exception
191     */
192    protected function createImage($width, $height)
193    {
194        // create a canvas to copy to, use truecolor if possible (except for gif)
195        $canvas = false;
196        if (function_exists('imagecreatetruecolor') && $this->extension != 'gif') {
197            $canvas = @imagecreatetruecolor($width, $height);
198        }
199        if (!$canvas) {
200            $canvas = @imagecreate($width, $height);
201        }
202        if (!$canvas) {
203            throw new Exception('Failed to create new canvas');
204        }
205
206        //keep png alpha channel if possible
207        if ($this->extension == 'png' && function_exists('imagesavealpha')) {
208            imagealphablending($canvas, false);
209            imagesavealpha($canvas, true);
210        }
211
212        //keep gif transparent color if possible
213        if ($this->extension == 'gif') {
214            $this->keepGifTransparency($this->image, $canvas);
215        }
216
217        return $canvas;
218    }
219
220    /**
221     * Copy transparency from gif to gif
222     *
223     * If no transparency is found or the PHP does not support it, the canvas is filled with white
224     *
225     * @param resource $image Original image
226     * @param resource $canvas New, empty image
227     * @return void
228     */
229    protected function keepGifTransparency($image, $canvas)
230    {
231        if (!function_exists('imagefill') || !function_exists('imagecolorallocate')) {
232            return;
233        }
234
235        try {
236            if (!function_exists('imagecolorsforindex') || !function_exists('imagecolortransparent')) {
237                throw new \Exception('missing alpha methods');
238            }
239
240            $transcolorindex = @imagecolortransparent($image);
241            $transcolor = @imagecolorsforindex($image, $transcolorindex);
242            if (!$transcolor) {
243                // pre-PHP8 false is returned, in PHP8 an exception is thrown
244                throw new \ValueError('no valid alpha color');
245            }
246
247            $transcolorindex = @imagecolorallocate(
248                $canvas,
249                $transcolor['red'],
250                $transcolor['green'],
251                $transcolor['blue']
252            );
253            @imagefill($canvas, 0, 0, $transcolorindex);
254            @imagecolortransparent($canvas, $transcolorindex);
255
256        } catch (\Throwable $ignored) {
257            //filling with white
258            $whitecolorindex = @imagecolorallocate($canvas, 255, 255, 255);
259            @imagefill($canvas, 0, 0, $whitecolorindex);
260        }
261    }
262
263    /**
264     * Calculates crop position
265     *
266     * Given the wanted final size, this calculates which exact area needs to be cut
267     * from the original image to be then resized to the wanted dimensions.
268     *
269     * @param int $width
270     * @param int $height
271     * @return array (cropWidth, cropHeight, offsetX, offsetY)
272     * @throws Exception
273     */
274    protected function cropPosition($width, $height)
275    {
276        if ($width == 0 && $height == 0) {
277            throw new Exception('You can not crop to 0x0');
278        }
279
280        if (!$height) {
281            $height = $width;
282        }
283
284        if (!$width) {
285            $width = $height;
286        }
287
288        // calculate ratios
289        $oldRatio = $this->width / $this->height;
290        $newRatio = $width / $height;
291
292        // calulate new size
293        if ($newRatio >= 1) {
294            if ($newRatio > $oldRatio) {
295                $cropWidth = $this->width;
296                $cropHeight = (int)($this->width / $newRatio);
297            } else {
298                $cropWidth = (int)($this->height * $newRatio);
299                $cropHeight = $this->height;
300            }
301        } else {
302            if ($newRatio < $oldRatio) {
303                $cropWidth = (int)($this->height * $newRatio);
304                $cropHeight = $this->height;
305            } else {
306                $cropWidth = $this->width;
307                $cropHeight = (int)($this->width / $newRatio);
308            }
309        }
310
311        // calculate crop offset
312        $offsetX = (int)(($this->width - $cropWidth) / 2);
313        $offsetY = (int)(($this->height - $cropHeight) / 2);
314
315        return [$cropWidth, $cropHeight, $offsetX, $offsetY];
316    }
317
318    /**
319     * resize or crop images using PHP's libGD support
320     *
321     * @param int $toWidth desired width
322     * @param int $toHeight desired height
323     * @param int $offsetX offset of crop centre
324     * @param int $offsetY offset of crop centre
325     * @throws Exception
326     */
327    protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0)
328    {
329        $newimg = $this->createImage($toWidth, $toHeight);
330
331        //try resampling first, fall back to resizing
332        if (
333            !function_exists('imagecopyresampled') ||
334            !@imagecopyresampled(
335                $newimg,
336                $this->image,
337                0,
338                0,
339                $offsetX,
340                $offsetY,
341                $toWidth,
342                $toHeight,
343                $this->width,
344                $this->height
345            )
346        ) {
347            imagecopyresized(
348                $newimg,
349                $this->image,
350                0,
351                0,
352                $offsetX,
353                $offsetY,
354                $toWidth,
355                $toHeight,
356                $this->width,
357                $this->height
358            );
359        }
360
361        // destroy original GD image ressource and replace with new one
362        $this->__destruct();
363        $this->image = $newimg;
364        $this->width = $toWidth;
365        $this->height = $toHeight;
366    }
367
368}
369