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        if ($width == 0 && $height == 0) {
272            throw new Exception('You can not resize to 0x0');
273        }
274
275        if (!$height) {
276            // adjust to match width
277            $height = round(($width * $this->height) / $this->width);
278        } else if (!$width) {
279            // adjust to match height
280            $width = round(($height * $this->width) / $this->height);
281        } else {
282            // fit into bounding box
283            $scale = min($width / $this->width, $height / $this->height);
284            $width = $this->width * $scale;
285            $height = $this->height * $scale;
286        }
287
288        return [$width, $height];
289    }
290
291    /**
292     * Calculates crop position
293     *
294     * Given the wanted final size, this calculates which exact area needs to be cut
295     * from the original image to be then resized to the wanted dimensions.
296     *
297     * @param int $width
298     * @param int $height
299     * @return array (cropWidth, cropHeight, offsetX, offsetY)
300     * @throws Exception
301     */
302    protected function cropPosition($width, $height)
303    {
304        if ($width == 0 && $height == 0) {
305            throw new Exception('You can not crop to 0x0');
306        }
307
308        if (!$height) {
309            $height = $width;
310        }
311
312        if (!$width) {
313            $width = $height;
314        }
315
316        // calculate ratios
317        $oldRatio = $this->width / $this->height;
318        $newRatio = $width / $height;
319
320        // calulate new size
321        if ($newRatio >= 1) {
322            if ($newRatio > $oldRatio) {
323                $cropWidth = $this->width;
324                $cropHeight = (int)($this->width / $newRatio);
325            } else {
326                $cropWidth = (int)($this->height * $newRatio);
327                $cropHeight = $this->height;
328            }
329        } else {
330            if ($newRatio < $oldRatio) {
331                $cropWidth = (int)($this->height * $newRatio);
332                $cropHeight = $this->height;
333            } else {
334                $cropWidth = $this->width;
335                $cropHeight = (int)($this->width / $newRatio);
336            }
337        }
338
339        // calculate crop offset
340        $offsetX = (int)(($this->width - $cropWidth) / 2);
341        $offsetY = (int)(($this->height - $cropHeight) / 2);
342
343        return [$cropWidth, $cropHeight, $offsetX, $offsetY];
344    }
345
346    /**
347     * resize or crop images using PHP's libGD support
348     *
349     * @param int $toWidth desired width
350     * @param int $toHeight desired height
351     * @param int $offsetX offset of crop centre
352     * @param int $offsetY offset of crop centre
353     * @throws Exception
354     */
355    protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0)
356    {
357        $newimg = $this->createImage($toWidth, $toHeight);
358
359        //try resampling first, fall back to resizing
360        if (
361            !function_exists('imagecopyresampled') ||
362            !@imagecopyresampled(
363                $newimg,
364                $this->image,
365                0,
366                0,
367                $offsetX,
368                $offsetY,
369                $toWidth,
370                $toHeight,
371                $this->width,
372                $this->height
373            )
374        ) {
375            imagecopyresized(
376                $newimg,
377                $this->image,
378                0,
379                0,
380                $offsetX,
381                $offsetY,
382                $toWidth,
383                $toHeight,
384                $this->width,
385                $this->height
386            );
387        }
388
389        // destroy original GD image ressource and replace with new one
390        imagedestroy($this->image);
391        $this->image = $newimg;
392        $this->width = $toWidth;
393        $this->height = $toHeight;
394    }
395
396}
397