1<?php /** @noinspection PhpComposerExtensionStubsInspection */
2
3
4namespace splitbrain\slika;
5
6
7class GdAdapter extends Adapter
8{
9    /** @var resource libGD image */
10    protected $image;
11    /** @var int width of the current image */
12    protected $width = 0;
13    /** @var int height of the current image */
14    protected $height = 0;
15    /** @var string the extension of the file we're working with */
16    protected $extension;
17
18
19    /** @inheritDoc */
20    public function __construct($imagepath, $options = [])
21    {
22        parent::__construct($imagepath, $options);
23        $this->image = $this->loadImage($imagepath);
24    }
25
26    /**
27     * Clean up
28     */
29    public function __destruct()
30    {
31        if (is_resource($this->image)) {
32            imagedestroy($this->image);
33        }
34    }
35
36    /** @inheritDoc
37     * @throws Exception
38     * @link https://gist.github.com/EionRobb/8e0c76178522bc963c75caa6a77d3d37#file-imagecreatefromstring_autorotate-php-L15
39     */
40    public function autorotate()
41    {
42        if ($this->extension !== 'jpeg') {
43            return $this;
44        }
45
46        $orientation = 1;
47
48        if (function_exists('exif_read_data')) {
49            // use PHP's exif capablities
50            $exif = exif_read_data($this->imagepath);
51            if (!empty($exif['Orientation'])) {
52                $orientation = $exif['Orientation'];
53            }
54        } else {
55            // grep the exif info from the raw contents
56            // we read only the first 70k bytes
57            $data = file_get_contents($this->imagepath, false, null, 0, 70000);
58            if (preg_match('@\x12\x01\x03\x00\x01\x00\x00\x00(.)\x00\x00\x00@', $data, $matches)) {
59                // Little endian EXIF
60                $orientation = ord($matches[1]);
61            } else if (preg_match('@\x01\x12\x00\x03\x00\x00\x00\x01\x00(.)\x00\x00@', $data, $matches)) {
62                // Big endian EXIF
63                $orientation = ord($matches[1]);
64            }
65        }
66
67        return $this->rotate($orientation);
68    }
69
70    /**
71     * @inheritDoc
72     * @throws Exception
73     */
74    public function rotate($orientation)
75    {
76        $orientation = (int)$orientation;
77        if ($orientation < 0 || $orientation > 8) {
78            throw new Exception('Unknown rotation given');
79        }
80
81        if ($orientation <= 1) {
82            // no rotation wanted
83            return $this;
84        }
85
86        // fill color
87        $transparency = imagecolorallocatealpha($this->image, 0, 0, 0, 127);
88
89        // rotate
90        if (in_array($orientation, [3, 4])) {
91            $image = imagerotate($this->image, 180, $transparency, 1);
92        }
93        if (in_array($orientation, [5, 6])) {
94            $image = imagerotate($this->image, -90, $transparency, 1);
95            list($this->width, $this->height) = [$this->height, $this->width];
96        } elseif (in_array($orientation, [7, 8])) {
97            $image = imagerotate($this->image, 90, $transparency, 1);
98            list($this->width, $this->height) = [$this->height, $this->width];
99        }
100        /** @var resource $image is now defined */
101
102        // additionally flip
103        if (in_array($orientation, [2, 5, 7, 4])) {
104            imageflip($image, IMG_FLIP_HORIZONTAL);
105        }
106
107        imagedestroy($this->image);
108        $this->image = $image;
109
110        //keep png alpha channel if possible
111        if ($this->extension == 'png' && function_exists('imagesavealpha')) {
112            imagealphablending($this->image, false);
113            imagesavealpha($this->image, true);
114        }
115
116        return $this;
117    }
118
119    /**
120     * @inheritDoc
121     * @throws Exception
122     */
123    public function resize($width, $height)
124    {
125        list($width, $height) = $this->boundingBox($width, $height);
126        $this->resizeOperation($width, $height);
127        return $this;
128    }
129
130    /**
131     * @inheritDoc
132     * @throws Exception
133     */
134    public function crop($width, $height)
135    {
136        list($this->width, $this->height, $offsetX, $offsetY) = $this->cropPosition($width, $height);
137        $this->resizeOperation($width, $height, $offsetX, $offsetY);
138        return $this;
139    }
140
141    /**
142     * @inheritDoc
143     * @throws Exception
144     */
145    public function save($path, $extension = '')
146    {
147        if ($extension === 'jpg') {
148            $extension = 'jpeg';
149        }
150        if ($extension === '') {
151            $extension = $this->extension;
152        }
153        $saver = 'image' . $extension;
154        if (!function_exists($saver)) {
155            throw new Exception('Can not save image format ' . $extension);
156        }
157
158        if ($extension == 'jpeg') {
159            imagejpeg($this->image, $path, $this->options['quality']);
160        } else {
161            $saver($this->image, $path);
162        }
163
164        imagedestroy($this->image);
165    }
166
167    /**
168     * Initialize libGD on the given image
169     *
170     * @param string $path
171     * @return resource
172     * @throws Exception
173     */
174    protected function loadImage($path)
175    {
176        // Figure out the file info
177        $info = getimagesize($path);
178        if ($info === false) {
179            throw new Exception('Failed to read image information');
180        }
181        $this->width = $info[0];
182        $this->height = $info[1];
183
184        // what type of image is it?
185        $this->extension = image_type_to_extension($info[2], false);
186        $creator = 'imagecreatefrom' . $this->extension;
187        if (!function_exists($creator)) {
188            throw new Exception('Can not work with image format ' . $this->extension);
189        }
190
191        // create the GD instance
192        $image = @$creator($path);
193
194        if ($image === false) {
195            throw new Exception('Failed to load image wiht libGD');
196        }
197
198        return $image;
199    }
200
201    /**
202     * Creates a new blank image to which we can copy
203     *
204     * Tries to set up alpha/transparency stuff correctly
205     *
206     * @param int $width
207     * @param int $height
208     * @return resource
209     * @throws Exception
210     */
211    protected function createImage($width, $height)
212    {
213        // create a canvas to copy to, use truecolor if possible (except for gif)
214        $canvas = false;
215        if (function_exists('imagecreatetruecolor') && $this->extension != 'gif') {
216            $canvas = @imagecreatetruecolor($width, $height);
217        }
218        if (!$canvas) {
219            $canvas = @imagecreate($width, $height);
220        }
221        if (!$canvas) {
222            throw new Exception('Failed to create new canvas');
223        }
224
225        //keep png alpha channel if possible
226        if ($this->extension == 'png' && function_exists('imagesavealpha')) {
227            imagealphablending($canvas, false);
228            imagesavealpha($canvas, true);
229        }
230
231        //keep gif transparent color if possible
232        if ($this->extension == 'gif' && function_exists('imagefill') && function_exists('imagecolorallocate')) {
233            if (function_exists('imagecolorsforindex') && function_exists('imagecolortransparent')) {
234                $transcolorindex = @imagecolortransparent($this->image);
235                if ($transcolorindex >= 0) { //transparent color exists
236                    $transcolor = @imagecolorsforindex($this->image, $transcolorindex);
237                    $transcolorindex = @imagecolorallocate(
238                        $canvas,
239                        $transcolor['red'],
240                        $transcolor['green'],
241                        $transcolor['blue']
242                    );
243                    @imagefill($canvas, 0, 0, $transcolorindex);
244                    @imagecolortransparent($canvas, $transcolorindex);
245                } else { //filling with white
246                    $whitecolorindex = @imagecolorallocate($canvas, 255, 255, 255);
247                    @imagefill($canvas, 0, 0, $whitecolorindex);
248                }
249            } else { //filling with white
250                $whitecolorindex = @imagecolorallocate($canvas, 255, 255, 255);
251                @imagefill($canvas, 0, 0, $whitecolorindex);
252            }
253        }
254
255        return $canvas;
256    }
257
258    /**
259     * Calculate new size
260     *
261     * If widht and height are given, the new size will be fit within this bounding box.
262     * If only one value is given the other is adjusted to match according to the aspect ratio
263     *
264     * @param int $width width of the bounding box
265     * @param int $height height of the bounding box
266     * @return array (width, height)
267     * @throws Exception
268     */
269    protected function boundingBox($width, $height)
270    {
271        $width = $this->cleanDimension($width, $this->width);
272        $height = $this->cleanDimension($height, $this->height);
273
274        if ($width == 0 && $height == 0) {
275            throw new Exception('You can not resize to 0x0');
276        }
277
278        if (!$height) {
279            // adjust to match width
280            $height = round(($width * $this->height) / $this->width);
281        } else if (!$width) {
282            // adjust to match height
283            $width = round(($height * $this->width) / $this->height);
284        } else {
285            // fit into bounding box
286            $scale = min($width / $this->width, $height / $this->height);
287            $width = $this->width * $scale;
288            $height = $this->height * $scale;
289        }
290
291        return [$width, $height];
292    }
293
294    /**
295     * Ensure the given Dimension is a proper pixel value
296     *
297     * When a percentage is given, the value is calculated based on the given original dimension
298     *
299     * @param int|string $dim New Dimension
300     * @param int $orig Original dimension
301     * @return int
302     */
303    protected function cleanDimension($dim, $orig)
304    {
305        if ($dim && substr($dim, -1) == '%') {
306            $dim = round($orig * ((float)$dim / 100));
307        } else {
308            $dim = (int)$dim;
309        }
310
311        return $dim;
312    }
313
314    /**
315     * Calculates crop position
316     *
317     * Given the wanted final size, this calculates which exact area needs to be cut
318     * from the original image to be then resized to the wanted dimensions.
319     *
320     * @param int $width
321     * @param int $height
322     * @return array (cropWidth, cropHeight, offsetX, offsetY)
323     * @throws Exception
324     */
325    protected function cropPosition($width, $height)
326    {
327        if ($width == 0 && $height == 0) {
328            throw new Exception('You can not crop to 0x0');
329        }
330
331        if (!$height) {
332            $height = $width;
333        }
334
335        if (!$width) {
336            $width = $height;
337        }
338
339        // calculate ratios
340        $oldRatio = $this->width / $this->height;
341        $newRatio = $width / $height;
342
343        // calulate new size
344        if ($newRatio >= 1) {
345            if ($newRatio > $oldRatio) {
346                $cropWidth = $this->width;
347                $cropHeight = (int)($this->width / $newRatio);
348            } else {
349                $cropWidth = (int)($this->height * $newRatio);
350                $cropHeight = $this->height;
351            }
352        } else {
353            if ($newRatio < $oldRatio) {
354                $cropWidth = (int)($this->height * $newRatio);
355                $cropHeight = $this->height;
356            } else {
357                $cropWidth = $this->width;
358                $cropHeight = (int)($this->width / $newRatio);
359            }
360        }
361
362        // calculate crop offset
363        $offsetX = (int)(($this->width - $cropWidth) / 2);
364        $offsetY = (int)(($this->height - $cropHeight) / 2);
365
366        return [$cropWidth, $cropHeight, $offsetX, $offsetY];
367    }
368
369    /**
370     * resize or crop images using PHP's libGD support
371     *
372     * @param int $toWidth desired width
373     * @param int $toHeight desired height
374     * @param int $offsetX offset of crop centre
375     * @param int $offsetY offset of crop centre
376     * @throws Exception
377     */
378    protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0)
379    {
380        $newimg = $this->createImage($toWidth, $toHeight);
381
382        //try resampling first, fall back to resizing
383        if (
384            !function_exists('imagecopyresampled') ||
385            !@imagecopyresampled(
386                $newimg,
387                $this->image,
388                0,
389                0,
390                $offsetX,
391                $offsetY,
392                $toWidth,
393                $toHeight,
394                $this->width,
395                $this->height
396            )
397        ) {
398            imagecopyresized(
399                $newimg,
400                $this->image,
401                0,
402                0,
403                $offsetX,
404                $offsetY,
405                $toWidth,
406                $toHeight,
407                $this->width,
408                $this->height
409            );
410        }
411
412        // destroy original GD image ressource and replace with new one
413        imagedestroy($this->image);
414        $this->image = $newimg;
415        $this->width = $toWidth;
416        $this->height = $toHeight;
417    }
418
419}
420