xref: /dokuwiki/vendor/splitbrain/slika/src/GdAdapter.php (revision 2d05a06d7893e8c100d5357c6301ffae08b00522) !
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, $upscale = true)
105    {
106        list($width, $height) = ImageInfo::boundingBox($this->width, $this->height, $width, $height, $upscale);
107        $this->resizeOperation($width, $height);
108        return $this;
109    }
110
111    /**
112     * @inheritDoc
113     * @throws Exception
114     */
115    public function crop($width, $height, $upscale = true)
116    {
117        $width = (int)$width;
118        $height = (int)$height;
119        if ($width == 0 && $height == 0) {
120            throw new Exception('You can not crop to 0x0');
121        }
122        // an omitted dimension results in a square
123        if (!$height) $height = $width;
124        if (!$width) $width = $height;
125
126        if (!$upscale && ($width > $this->width || $height > $this->height)) {
127            // never enlarge: crop the oversized dimension(s) centered, without scaling
128            $cropWidth = min($width, $this->width);
129            $cropHeight = min($height, $this->height);
130            list($offsetX, $offsetY) = $this->centerOffset($cropWidth, $cropHeight);
131            // the extracted region equals the output, so nothing is scaled
132            $width = $cropWidth;
133            $height = $cropHeight;
134        } else {
135            list($cropWidth, $cropHeight, $offsetX, $offsetY) = $this->cropPosition($width, $height);
136        }
137
138        $this->width = $cropWidth;
139        $this->height = $cropHeight;
140        $this->resizeOperation($width, $height, $offsetX, $offsetY);
141        return $this;
142    }
143
144    /**
145     * @inheritDoc
146     * @throws Exception
147     */
148    public function save($path, $extension = '')
149    {
150        if ($extension === 'jpg') {
151            $extension = 'jpeg';
152        }
153        if ($extension === '') {
154            $extension = $this->extension;
155        }
156        $saver = 'image' . $extension;
157        if (!function_exists($saver)) {
158            throw new Exception('Can not save image format ' . $extension);
159        }
160
161        if ($extension == 'jpeg') {
162            imagejpeg($this->image, $path, $this->options['quality']);
163        } else {
164            $saver($this->image, $path);
165        }
166
167        $this->__destruct();
168    }
169
170    /**
171     * Initialize libGD on the given image
172     *
173     * @param string $path
174     * @return resource
175     * @throws Exception
176     */
177    protected function loadImage($path)
178    {
179        // Figure out the file info
180        $info = getimagesize($path);
181        if ($info === false) {
182            throw new Exception('Failed to read image information');
183        }
184        $this->width = $info[0];
185        $this->height = $info[1];
186
187        // what type of image is it?
188        $this->extension = image_type_to_extension($info[2], false);
189        $creator = 'imagecreatefrom' . $this->extension;
190        if (!function_exists($creator)) {
191            throw new Exception('Can not work with image format ' . $this->extension);
192        }
193
194        // create the GD instance
195        $image = @$creator($path);
196
197        if ($image === false) {
198            throw new Exception('Failed to load image wiht libGD');
199        }
200
201        return $image;
202    }
203
204    /**
205     * Creates a new blank image to which we can copy
206     *
207     * Tries to set up alpha/transparency stuff correctly
208     *
209     * @param int $width
210     * @param int $height
211     * @return resource
212     * @throws Exception
213     */
214    protected function createImage($width, $height)
215    {
216        // create a canvas to copy to, use truecolor if possible (except for gif)
217        $canvas = false;
218        if (function_exists('imagecreatetruecolor') && $this->extension != 'gif') {
219            $canvas = @imagecreatetruecolor($width, $height);
220        }
221        if (!$canvas) {
222            $canvas = @imagecreate($width, $height);
223        }
224        if (!$canvas) {
225            throw new Exception('Failed to create new canvas');
226        }
227
228        //keep png alpha channel if possible
229        if ($this->extension == 'png' && function_exists('imagesavealpha')) {
230            imagealphablending($canvas, false);
231            imagesavealpha($canvas, true);
232        }
233
234        //keep gif transparent color if possible
235        if ($this->extension == 'gif') {
236            $this->keepGifTransparency($this->image, $canvas);
237        }
238
239        return $canvas;
240    }
241
242    /**
243     * Copy transparency from gif to gif
244     *
245     * If no transparency is found or the PHP does not support it, the canvas is filled with white
246     *
247     * @param resource $image Original image
248     * @param resource $canvas New, empty image
249     * @return void
250     */
251    protected function keepGifTransparency($image, $canvas)
252    {
253        if (!function_exists('imagefill') || !function_exists('imagecolorallocate')) {
254            return;
255        }
256
257        try {
258            if (!function_exists('imagecolorsforindex') || !function_exists('imagecolortransparent')) {
259                throw new \Exception('missing alpha methods');
260            }
261
262            $transcolorindex = @imagecolortransparent($image);
263            $transcolor = @imagecolorsforindex($image, $transcolorindex);
264            if (!$transcolor) {
265                // pre-PHP8 false is returned, in PHP8 an exception is thrown
266                throw new \ValueError('no valid alpha color');
267            }
268
269            $transcolorindex = @imagecolorallocate(
270                $canvas,
271                $transcolor['red'],
272                $transcolor['green'],
273                $transcolor['blue']
274            );
275            @imagefill($canvas, 0, 0, $transcolorindex);
276            @imagecolortransparent($canvas, $transcolorindex);
277
278        } catch (\Throwable $ignored) {
279            //filling with white
280            $whitecolorindex = @imagecolorallocate($canvas, 255, 255, 255);
281            @imagefill($canvas, 0, 0, $whitecolorindex);
282        }
283    }
284
285    /**
286     * Calculates crop position
287     *
288     * Given the wanted final size, this calculates which exact area needs to be cut
289     * from the original image to be then resized to the wanted dimensions.
290     *
291     * The dimensions are expected to be already resolved to non-zero pixel values.
292     *
293     * @param int $width resolved target width
294     * @param int $height resolved target height
295     * @return array (cropWidth, cropHeight, offsetX, offsetY)
296     */
297    protected function cropPosition($width, $height)
298    {
299        // calculate ratios
300        $oldRatio = $this->width / $this->height;
301        $newRatio = $width / $height;
302
303        // calulate new size
304        if ($newRatio >= 1) {
305            if ($newRatio > $oldRatio) {
306                $cropWidth = $this->width;
307                $cropHeight = (int)($this->width / $newRatio);
308            } else {
309                $cropWidth = (int)($this->height * $newRatio);
310                $cropHeight = $this->height;
311            }
312        } else {
313            if ($newRatio < $oldRatio) {
314                $cropWidth = (int)($this->height * $newRatio);
315                $cropHeight = $this->height;
316            } else {
317                $cropWidth = $this->width;
318                $cropHeight = (int)($this->width / $newRatio);
319            }
320        }
321
322        list($offsetX, $offsetY) = $this->centerOffset($cropWidth, $cropHeight);
323
324        return [$cropWidth, $cropHeight, $offsetX, $offsetY];
325    }
326
327    /**
328     * Calculate the top left offset to extract a centered region of the given size
329     *
330     * @param int $cropWidth width of the region to extract
331     * @param int $cropHeight height of the region to extract
332     * @return array (offsetX, offsetY)
333     */
334    protected function centerOffset($cropWidth, $cropHeight)
335    {
336        return [
337            (int)(($this->width - $cropWidth) / 2),
338            (int)(($this->height - $cropHeight) / 2),
339        ];
340    }
341
342    /**
343     * resize or crop images using PHP's libGD support
344     *
345     * @param int $toWidth desired width
346     * @param int $toHeight desired height
347     * @param int $offsetX offset of crop centre
348     * @param int $offsetY offset of crop centre
349     * @throws Exception
350     */
351    protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0)
352    {
353        $newimg = $this->createImage($toWidth, $toHeight);
354
355        //try resampling first, fall back to resizing
356        if (
357            !function_exists('imagecopyresampled') ||
358            !@imagecopyresampled(
359                $newimg,
360                $this->image,
361                0,
362                0,
363                $offsetX,
364                $offsetY,
365                $toWidth,
366                $toHeight,
367                $this->width,
368                $this->height
369            )
370        ) {
371            imagecopyresized(
372                $newimg,
373                $this->image,
374                0,
375                0,
376                $offsetX,
377                $offsetY,
378                $toWidth,
379                $toHeight,
380                $this->width,
381                $this->height
382            );
383        }
384
385        // destroy original GD image ressource and replace with new one
386        $this->__destruct();
387        $this->image = $newimg;
388        $this->width = $toWidth;
389        $this->height = $toHeight;
390    }
391
392}
393