1<?php
2/**
3 * 2007-2016 PrestaShop
4 *
5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA
6 * Copyright (C) 2017-2018 thirty bees
7 *
8 * NOTICE OF LICENSE
9 *
10 * This source file is subject to the Open Software License (OSL 3.0)
11 * that is bundled with this package in the file LICENSE.txt.
12 * It is also available through the world-wide-web at this URL:
13 * http://opensource.org/licenses/osl-3.0.php
14 * If you did not receive a copy of the license and are unable to
15 * obtain it through the world-wide-web, please send an email
16 * to license@thirtybees.com so we can send you a copy immediately.
17 *
18 * DISCLAIMER
19 *
20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
21 * versions in the future. If you wish to customize PrestaShop for your
22 * needs please refer to https://www.thirtybees.com for more information.
23 *
24 * @author    thirty bees <contact@thirtybees.com>
25 * @author    PrestaShop SA <contact@prestashop.com>
26 * @copyright 2017-2018 thirty bees
27 * @copyright 2007-2016 PrestaShop SA
28 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
29 *  PrestaShop is an internationally registered trademark & property of PrestaShop SA
30 */
31
32/**
33 * Class ImageManagerCore
34 *
35 * @since 1.0.0
36 */
37class ImageManagerCore
38{
39    const ERROR_FILE_NOT_EXIST = 1;
40    const ERROR_FILE_WIDTH = 2;
41    const ERROR_MEMORY_LIMIT = 3;
42
43    /**
44     * Generate a cached thumbnail for object lists (eg. carrier, order statuses...etc)
45     *
46     * @param string $image        Real image filename
47     * @param string $cacheImage   Cached filename
48     * @param int    $size         Desired size
49     * @param string $imageType    Image type
50     * @param bool   $disableCache When turned on a timestamp will be added to the image URI to disable the HTTP cache
51     * @param bool   $regenerate   When turned on and the file already exist, the file will be regenerated
52     *
53     * @return string
54     *
55     * @since   1.0.0
56     * @version 1.0.0 Initial version
57     */
58    public static function thumbnail($image, $cacheImage, $size, $imageType = 'jpg', $disableCache = true, $regenerate = false)
59    {
60        if (!file_exists($image)) {
61            return '';
62        }
63
64        if (file_exists(_PS_TMP_IMG_DIR_.$cacheImage) && $regenerate) {
65            @unlink(_PS_TMP_IMG_DIR_.$cacheImage);
66        }
67
68        if ($regenerate || !file_exists(_PS_TMP_IMG_DIR_.$cacheImage)) {
69            $infos = getimagesize($image);
70
71            // Evaluate the memory required to resize the image: if it's too much, you can't resize it.
72            if (!ImageManager::checkImageMemoryLimit($image)) {
73                return false;
74            }
75
76            $x = $infos[0];
77            $y = $infos[1];
78            $maxX = $size * 3;
79
80            // Size is already ok
81            if ($y < $size && $x <= $maxX) {
82                copy($image, _PS_TMP_IMG_DIR_.$cacheImage);
83            } // We need to resize */
84            else {
85                $ratio_x = $x / ($y / $size);
86                if ($ratio_x > $maxX) {
87                    $ratio_x = $maxX;
88                    $size = $y / ($x / $maxX);
89                }
90
91                ImageManager::resize($image, _PS_TMP_IMG_DIR_.$cacheImage, $ratio_x, $size, $imageType);
92            }
93        }
94        // Relative link will always work, whatever the base uri set in the admin
95        if (Context::getContext()->controller->controller_type == 'admin') {
96            return '<img src="../img/tmp/'.$cacheImage.($disableCache ? '?time='.time() : '').'" alt="" class="imgm img-thumbnail" />';
97        } else {
98            return '<img src="'._PS_TMP_IMG_.$cacheImage.($disableCache ? '?time='.time() : '').'" alt="" class="imgm img-thumbnail" />';
99        }
100    }
101
102    /**
103     * Check if memory limit is too long or not
104     *
105     * @param $image
106     *
107     * @return bool
108     *
109     * @since   1.0.0
110     * @version 1.0.0 Initial version
111     */
112    public static function checkImageMemoryLimit($image)
113    {
114        $infos = @getimagesize($image);
115
116        if (!is_array($infos) || !isset($infos['bits'])) {
117            return true;
118        }
119
120        $memoryLimit = Tools::getMemoryLimit();
121        // memory_limit == -1 => unlimited memory
122        if (isset($infos['bits']) && function_exists('memory_get_usage') && (int) $memoryLimit != -1) {
123            $currentMemory = memory_get_usage();
124            $bits = $infos['bits'] / 8;
125            $channel = isset($infos['channels']) ? $infos['channels'] : 1;
126
127            // Evaluate the memory required to resize the image: if it's too much, you can't resize it.
128            // For perfs, avoid computing static maths formulas in the code. pow(2, 16) = 65536 ; 1024 * 1024 = 1048576
129            if (($infos[0] * $infos[1] * $bits * $channel + 65536) * 1.8 + $currentMemory > $memoryLimit - 1048576) {
130                return false;
131            }
132        }
133
134        return true;
135    }
136
137    /**
138     * Resize, cut and optimize image
139     *
140     * @param string $srcFile   Image object from $_FILE
141     * @param string $dstFile   Destination filename
142     * @param int    $dstWidth  Desired width (optional)
143     * @param int    $dstHeight Desired height (optional)
144     * @param string $fileType
145     * @param bool   $forceType
146     * @param int    $error
147     * @param int    $tgtWidth
148     * @param int    $tgtHeight
149     * @param int    $quality
150     * @param int    $srcWidth
151     * @param int    $srcHeight
152     *
153     * @return bool Operation result
154     *
155     * @since   1.0.0
156     * @version 1.0.0 Initial version
157     * @throws PrestaShopException
158     */
159    public static function resize(
160        $srcFile,
161        $dstFile,
162        $dstWidth = null,
163        $dstHeight = null,
164        $fileType = 'jpg',
165        $forceType = false,
166        &$error = 0,
167        &$tgtWidth = null,
168        &$tgtHeight = null,
169        $quality = 5,
170        &$srcWidth = null,
171        &$srcHeight = null
172    ) {
173        clearstatcache(true, $srcFile);
174
175        if (!file_exists($srcFile) || !filesize($srcFile)) {
176            return !($error = static::ERROR_FILE_NOT_EXIST);
177        }
178
179        list($tmpWidth, $tmpHeight, $type) = getimagesize($srcFile);
180        $rotate = 0;
181        if (function_exists('exif_read_data') && function_exists('mb_strtolower')) {
182            $exif = @exif_read_data($srcFile);
183
184            if ($exif && isset($exif['Orientation'])) {
185                switch ($exif['Orientation']) {
186                    case 3:
187                        $srcWidth = $tmpWidth;
188                        $srcHeight = $tmpHeight;
189                        $rotate = 180;
190                        break;
191
192                    case 6:
193                        $srcWidth = $tmpHeight;
194                        $srcHeight = $tmpWidth;
195                        $rotate = -90;
196                        break;
197
198                    case 8:
199                        $srcWidth = $tmpHeight;
200                        $srcHeight = $tmpWidth;
201                        $rotate = 90;
202                        break;
203
204                    default:
205                        $srcWidth = $tmpWidth;
206                        $srcHeight = $tmpHeight;
207                }
208            } else {
209                $srcWidth = $tmpWidth;
210                $srcHeight = $tmpHeight;
211            }
212        } else {
213            $srcWidth = $tmpWidth;
214            $srcHeight = $tmpHeight;
215        }
216
217        // If PS_IMAGE_QUALITY is activated, the generated image will be a PNG with .jpg as a file extension.
218        // This allow for higher quality and for transparency. JPG source files will also benefit from a higher quality
219        // because JPG reencoding by GD, even with max quality setting, degrades the image.
220        if ($fileType !== 'webp' && (Configuration::get('PS_IMAGE_QUALITY') == 'png_all'
221            || (Configuration::get('PS_IMAGE_QUALITY') == 'png' && $type == IMAGETYPE_PNG) && !$forceType)
222        ) {
223            $fileType = 'png';
224        }
225
226        if (!$srcWidth) {
227            return !($error = static::ERROR_FILE_WIDTH);
228        }
229        if (!$dstWidth) {
230            $dstWidth = $srcWidth;
231        }
232        if (!$dstHeight) {
233            $dstHeight = $srcHeight;
234        }
235
236        $widthDiff = $dstWidth / $srcWidth;
237        $heightDiff = $dstHeight / $srcHeight;
238
239        $psImageGenerationMethod = Configuration::get('PS_IMAGE_GENERATION_METHOD');
240        if ($widthDiff > 1 && $heightDiff > 1) {
241            $nextWidth = $srcWidth;
242            $nextHeight = $srcHeight;
243        } else {
244            if ($psImageGenerationMethod == 2 || (!$psImageGenerationMethod && $widthDiff > $heightDiff)) {
245                $nextHeight = $dstHeight;
246                $nextWidth = round(($srcWidth * $nextHeight) / $srcHeight);
247                $dstWidth = (int) (!$psImageGenerationMethod ? $dstWidth : $nextWidth);
248            } else {
249                $nextWidth = $dstWidth;
250                $nextHeight = round($srcHeight * $dstWidth / $srcWidth);
251                $dstHeight = (int) (!$psImageGenerationMethod ? $dstHeight : $nextHeight);
252            }
253        }
254
255        if (!ImageManager::checkImageMemoryLimit($srcFile)) {
256            return !($error = static::ERROR_MEMORY_LIMIT);
257        }
258
259        $tgtWidth = $dstWidth;
260        $tgtHeight = $dstHeight;
261
262        $destImage = imagecreatetruecolor($dstWidth, $dstHeight);
263
264        // If image is a PNG or WEBP and the output is PNG/WEBP, fill with transparency. Else fill with white background.
265        if ($fileType == 'png' && $type == IMAGETYPE_PNG || $fileType === 'webp') {
266            imagealphablending($destImage, false);
267            imagesavealpha($destImage, true);
268            $transparent = imagecolorallocatealpha($destImage, 255, 255, 255, 127);
269            imagefilledrectangle($destImage, 0, 0, $dstWidth, $dstHeight, $transparent);
270        } else {
271            $white = imagecolorallocate($destImage, 255, 255, 255);
272            imagefilledrectangle($destImage, 0, 0, $dstWidth, $dstHeight, $white);
273        }
274
275        $srcImage = ImageManager::create($type, $srcFile);
276        if ($rotate) {
277            $srcImage = imagerotate($srcImage, $rotate, 0);
278        }
279
280        if ($dstWidth >= $srcWidth && $dstHeight >= $srcHeight) {
281            imagecopyresized($destImage, $srcImage, (int) (($dstWidth - $nextWidth) / 2), (int) (($dstHeight - $nextHeight) / 2), 0, 0, $nextWidth, $nextHeight, $srcWidth, $srcHeight);
282        } else {
283            ImageManager::imagecopyresampled($destImage, $srcImage, (int) (($dstWidth - $nextWidth) / 2), (int) (($dstHeight - $nextHeight) / 2), 0, 0, $nextWidth, $nextHeight, $srcWidth, $srcHeight, $quality);
284        }
285        $writeFile = ImageManager::write($fileType, $destImage, $dstFile);
286        @imagedestroy($srcImage);
287
288        return $writeFile;
289    }
290
291    /**
292     * Create an image with GD extension from a given type
293     *
294     * @param string $type
295     * @param string $filename
296     *
297     * @return resource
298     *
299     * @since   1.0.0
300     * @version 1.0.0 Initial version
301     */
302    public static function create($type, $filename)
303    {
304        switch ($type) {
305            case IMAGETYPE_GIF :
306                return imagecreatefromgif($filename);
307
308            case IMAGETYPE_PNG :
309                return imagecreatefrompng($filename);
310
311            case 18:
312                return imagecreatefromwebp($filename);
313
314            case IMAGETYPE_JPEG :
315            default:
316                return imagecreatefromjpeg($filename);
317                break;
318        }
319    }
320
321    /**
322     * @param     $dstImage
323     * @param     $srcImage
324     * @param     $dstX
325     * @param     $dstY
326     * @param     $srcX
327     * @param     $srcY
328     * @param     $dstW
329     * @param     $dstH
330     * @param     $srcW
331     * @param     $srcH
332     * @param int $quality
333     *
334     * @return bool
335     *
336     * @since   1.0.0
337     * @version 1.0.0 Initial version
338     */
339    public static function imagecopyresampled(&$dstImage, $srcImage, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH, $quality = 3)
340    {
341        // Plug-and-Play fastimagecopyresampled function replaces much slower imagecopyresampled.
342        // Just include this function and change all "imagecopyresampled" references to "fastimagecopyresampled".
343        // Typically from 30 to 60 times faster when reducing high resolution images down to thumbnail size using the default quality setting.
344        // Author: Tim Eckel - Date: 09/07/07 - Version: 1.1 - Project: FreeRingers.net - Freely distributable - These comments must remain.
345        //
346        // Optional "quality" parameter (defaults is 3). Fractional values are allowed, for example 1.5. Must be greater than zero.
347        // Between 0 and 1 = Fast, but mosaic results, closer to 0 increases the mosaic effect.
348        // 1 = Up to 350 times faster. Poor results, looks very similar to imagecopyresized.
349        // 2 = Up to 95 times faster.  Images appear a little sharp, some prefer this over a quality of 3.
350        // 3 = Up to 60 times faster.  Will give high quality smooth results very close to imagecopyresampled, just faster.
351        // 4 = Up to 25 times faster.  Almost identical to imagecopyresampled for most images.
352        // 5 = No speedup. Just uses imagecopyresampled, no advantage over imagecopyresampled.
353
354        if (empty($srcImage) || empty($dstImage) || $quality <= 0) {
355            return false;
356        }
357        if ($quality < 5 && (($dstW * $quality) < $srcW || ($dstH * $quality) < $srcH)) {
358            $temp = imagecreatetruecolor($dstW * $quality + 1, $dstH * $quality + 1);
359            imagecopyresized($temp, $srcImage, 0, 0, $srcX, $srcY, $dstW * $quality + 1, $dstH * $quality + 1, $srcW, $srcH);
360            imagecopyresampled($dstImage, $temp, $dstX, $dstY, 0, 0, $dstW, $dstH, $dstW * $quality, $dstH * $quality);
361            imagedestroy($temp);
362        } else {
363            imagecopyresampled($dstImage, $srcImage, $dstX, $dstY, $srcX, $srcY, $dstW, $dstH, $srcW, $srcH);
364        }
365
366        return true;
367    }
368
369    /**
370     * Generate and write image
371     *
372     * @param string   $type
373     * @param resource $resource
374     * @param string   $filename
375     *
376     * @return bool
377     *
378     * @since   1.0.0
379     * @version 1.0.0 Initial version
380     * @throws PrestaShopException
381     */
382    public static function write($type, $resource, $filename)
383    {
384        static $psPngQuality = null;
385        static $psJpegQuality = null;
386        static $psWebpQuality = null;
387
388        if ($psPngQuality === null) {
389            $psPngQuality = Configuration::get('PS_PNG_QUALITY');
390        }
391
392        if ($psJpegQuality === null) {
393            $psJpegQuality = Configuration::get('PS_JPEG_QUALITY');
394        }
395
396        if ($psWebpQuality === null) {
397            $psWebpQuality = Configuration::get('PS_WEBP_QUALITY');
398        }
399
400        switch ($type) {
401            case 'gif':
402                $success = imagegif($resource, $filename);
403                break;
404
405            case 'png':
406                $quality = ($psPngQuality === false ? 7 : $psPngQuality);
407                $success = imagepng($resource, $filename, (int) $quality);
408                break;
409
410            case 'webp':
411                $quality = ($psWebpQuality === false ? 90 : $psWebpQuality);
412                $success = imagewebp($resource, $filename, (int) $quality);
413                break;
414
415            case 'jpg':
416            case 'jpeg':
417            default:
418                $quality = ($psJpegQuality === false ? 90 : $psJpegQuality);
419                imageinterlace($resource, 1); /// make it PROGRESSIVE
420                $success = imagejpeg($resource, $filename, (int) $quality);
421                break;
422        }
423        imagedestroy($resource);
424        @chmod($filename, 0664);
425
426        return $success;
427    }
428
429    /**
430     * Validate image upload (check image type and weight)
431     *
432     * @param array $file        Upload $_FILE value
433     * @param int   $maxFileSize Maximum upload size
434     *
435     * @return bool|string Return false if no error encountered
436     *
437     * @since   1.0.0
438     * @version 1.0.0 Initial version
439     */
440    public static function validateUpload($file, $maxFileSize = 0, $types = null)
441    {
442        if ((int) $maxFileSize > 0 && $file['size'] > (int) $maxFileSize) {
443            return sprintf(Tools::displayError('Image is too large (%1$d kB). Maximum allowed: %2$d kB'), $file['size'] / 1024, $maxFileSize / 1024);
444        }
445        if (!ImageManager::isRealImage($file['tmp_name'], $file['type']) || !ImageManager::isCorrectImageFileExt($file['name'], $types) || preg_match('/\%00/', $file['name'])) {
446            return Tools::displayError('Image format not recognized, allowed formats are: .gif, .jpg, .png');
447        }
448        if ($file['error']) {
449            return sprintf(Tools::displayError('Error while uploading image; please change your server\'s settings. (Error code: %s)'), $file['error']);
450        }
451
452        return false;
453    }
454
455    /**
456     * Check if file is a real image
457     *
458     * @param string $filename     File path to check
459     * @param string $fileMimeType File known mime type (generally from $_FILES)
460     * @param array  $mimeTypeList Allowed MIME types
461     *
462     * @return bool
463     *
464     * @since   1.0.0
465     * @version 1.0.0 Initial version
466     */
467    public static function isRealImage($filename, $fileMimeType = null, $mimeTypeList = null)
468    {
469        // Detect mime content type
470        $mimeType = false;
471        if (!$mimeTypeList) {
472            $mimeTypeList = ['image/gif', 'image/jpg', 'image/jpeg', 'image/pjpeg', 'image/png', 'image/x-png'];
473        }
474
475        // Try 4 different methods to determine the mime type
476        if (function_exists('getimagesize')) {
477            $imageInfo = @getimagesize($filename);
478
479            if ($imageInfo) {
480                $mimeType = $imageInfo['mime'];
481            } else {
482                $fileMimeType = false;
483            }
484        } elseif (function_exists('finfo_open')) {
485            $const = defined('FILEINFO_MIME_TYPE') ? FILEINFO_MIME_TYPE : FILEINFO_MIME;
486            $finfo = finfo_open($const);
487            $mimeType = finfo_file($finfo, $filename);
488            finfo_close($finfo);
489        } elseif (function_exists('mime_content_type')) {
490            $mimeType = mime_content_type($filename);
491        } elseif (function_exists('exec')) {
492            $mimeType = trim(exec('file -b --mime-type '.escapeshellarg($filename)));
493            if (!$mimeType) {
494                $mimeType = trim(exec('file --mime '.escapeshellarg($filename)));
495            }
496            if (!$mimeType) {
497                $mimeType = trim(exec('file -bi '.escapeshellarg($filename)));
498            }
499        }
500
501        if ($fileMimeType && (empty($mimeType) || $mimeType == 'regular file' || $mimeType == 'text/plain')) {
502            $mimeType = $fileMimeType;
503        }
504
505        // For each allowed MIME type, we are looking for it inside the current MIME type
506        foreach ($mimeTypeList as $type) {
507            if (strstr($mimeType, $type)) {
508                return true;
509            }
510        }
511
512        return false;
513    }
514
515    /**
516     * Check if image file extension is correct
517     *
518     * @param string     $filename Real filename
519     * @param array|null $authorizedExtensions
520     *
521     * @return bool True if it's correct
522     *
523     * @since   1.0.0
524     * @version 1.0.0 Initial version
525     */
526    public static function isCorrectImageFileExt($filename, $authorizedExtensions = null)
527    {
528        // Filter on file extension
529        if ($authorizedExtensions === null) {
530            $authorizedExtensions = ['gif', 'jpg', 'jpeg', 'jpe', 'png'];
531        }
532        $nameExplode = explode('.', $filename);
533        if (count($nameExplode) >= 2) {
534            $current_extension = strtolower($nameExplode[count($nameExplode) - 1]);
535            if (!in_array($current_extension, $authorizedExtensions)) {
536                return false;
537            }
538        } else {
539            return false;
540        }
541
542        return true;
543    }
544
545    /**
546     * Validate icon upload
547     *
548     * @param array $file        Upload $_FILE value
549     * @param int   $maxFileSize Maximum upload size
550     *
551     * @return bool|string Return false if no error encountered
552     *
553     * @since   1.0.0
554     * @version 1.0.0 Initial version
555     */
556    public static function validateIconUpload($file, $maxFileSize = 0)
557    {
558        if ((int) $maxFileSize > 0 && $file['size'] > $maxFileSize) {
559            return sprintf(
560                Tools::displayError('Image is too large (%1$d kB). Maximum allowed: %2$d kB'),
561                $file['size'] / 1000,
562                $maxFileSize / 1000
563            );
564        }
565        if (substr($file['name'], -4) != '.ico' && substr($file['name'], -4) != '.png') {
566            return Tools::displayError('Image format not recognized, allowed formats are: .ico, .png');
567        }
568        if ($file['error']) {
569            return Tools::displayError('Error while uploading image; please change your server\'s settings.');
570        }
571
572        return false;
573    }
574
575    /**
576     * Cut image
577     *
578     * @param array  $srcFile   Origin filename
579     * @param string $dstFile   Destination filename
580     * @param int    $dstWidth  Desired width
581     * @param int    $dstHeight Desired height
582     * @param string $fileType
583     * @param int    $dstX
584     * @param int    $dstY
585     *
586     * @return bool Operation result
587     *
588     * @since   1.0.0
589     * @version 1.0.0 Initial version
590     * @throws PrestaShopException
591     */
592    public static function cut($srcFile, $dstFile, $dstWidth = null, $dstHeight = null, $fileType = 'jpg', $dstX = 0, $dstY = 0)
593    {
594        if (!file_exists($srcFile)) {
595            return false;
596        }
597
598        // Source information
599        $srcInfo = getimagesize($srcFile);
600        $src = [
601            'width'     => $srcInfo[0],
602            'height'    => $srcInfo[1],
603            'ressource' => ImageManager::create($srcInfo[2], $srcFile),
604        ];
605
606        // Destination information
607        $dest = [];
608        $dest['x'] = $dstX;
609        $dest['y'] = $dstY;
610        $dest['width'] = !is_null($dstWidth) ? $dstWidth : $src['width'];
611        $dest['height'] = !is_null($dstHeight) ? $dstHeight : $src['height'];
612        $dest['ressource'] = ImageManager::createWhiteImage($dest['width'], $dest['height']);
613
614        $white = imagecolorallocate($dest['ressource'], 255, 255, 255);
615        imagecopyresampled($dest['ressource'], $src['ressource'], 0, 0, $dest['x'], $dest['y'], $dest['width'], $dest['height'], $dest['width'], $dest['height']);
616        imagecolortransparent($dest['ressource'], $white);
617        $return = ImageManager::write($fileType, $dest['ressource'], $dstFile);
618        @imagedestroy($src['ressource']);
619
620        return $return;
621    }
622
623    /**
624     * Create an empty image with white background
625     *
626     * @param int $width
627     * @param int $height
628     *
629     * @return resource
630     *
631     * @since   1.0.0
632     * @version 1.0.0 Initial version
633     */
634    public static function createWhiteImage($width, $height)
635    {
636        $image = imagecreatetruecolor($width, $height);
637        $white = imagecolorallocate($image, 255, 255, 255);
638        imagefill($image, 0, 0, $white);
639
640        return $image;
641    }
642
643    /**
644     * Return the mime type by the file extension
645     *
646     * @param string $fileName
647     *
648     * @return string
649     *
650     * @since   1.0.0
651     * @version 1.0.0 Initial version
652     */
653    public static function getMimeTypeByExtension($fileName)
654    {
655        $types = [
656            'image/gif'  => ['gif'],
657            'image/jpeg' => ['jpg', 'jpeg'],
658            'image/png'  => ['png'],
659        ];
660        $extension = substr($fileName, strrpos($fileName, '.') + 1);
661
662        $mimeType = null;
663        foreach ($types as $mime => $exts) {
664            if (in_array($extension, $exts)) {
665                $mimeType = $mime;
666                break;
667            }
668        }
669
670        if ($mimeType === null) {
671            $mimeType = 'image/jpeg';
672        }
673
674        return $mimeType;
675    }
676
677
678    /**
679     * Add an image to the generator.
680     *
681     * This function adds a source image to the generator. It serves two main purposes: add a source image if one was
682     * not supplied to the constructor and to add additional source images so that different images can be supplied for
683     * different sized images in the resulting ICO file. For instance, a small source image can be used for the small
684     * resolutions while a larger source image can be used for large resolutions.
685     *
686     * @param string $source Path to the source image file.
687     * @param array  $sizes  Optional. An array of sizes (each size is an array with a width and height) that the source image should be rendered at in the generated ICO file. If sizes are not supplied, the size of the source image will be used.
688     *
689     * @return boolean true on success and false on failure.
690     *
691     * @copyright 2011-2016  Chris Jean
692     * @author Chris Jean
693     * @license GNU General Public License v2.0
694     * @source https://github.com/chrisbliss18/php-ico
695     */
696    public static function generateFavicon($source, $sizes = [['16', '16'], ['24', '24'], ['32', '32'], ['48', '48'], ['64', '64']])
697    {
698        $images = [];
699
700        if (!$size = getimagesize($source)) {
701            return false;
702        }
703        if (!$file_data = file_get_contents($source)) {
704            return false;
705        }
706        if (!$im = imagecreatefromstring($file_data)) {
707            return false;
708        }
709        unset($file_data);
710        if (empty($sizes)) {
711            $sizes = [imagesx($im), imagesy($im)];
712        }
713
714        // If just a single size was passed, put it in array.
715        if ( ! is_array( $sizes[0] ) ) {
716            $sizes = [$sizes];
717        }
718
719        foreach ( (array) $sizes as $size ) {
720            list( $width, $height ) = $size;
721            $new_im = imagecreatetruecolor( $width, $height );
722            imagecolortransparent( $new_im, imagecolorallocatealpha( $new_im, 0, 0, 0, 127 ) );
723            imagealphablending( $new_im, false );
724            imagesavealpha( $new_im, true );
725            $source_width = imagesx( $im );
726            $source_height = imagesy( $im );
727            if ( false === imagecopyresampled( $new_im, $im, 0, 0, 0, 0, $width, $height, $source_width, $source_height ) )
728                continue;
729
730            static::addFaviconImageData($new_im, $images);
731        }
732
733        return static::getIcoData($images);
734    }
735
736    /**
737     * Generate the final ICO data by creating a file header and adding the image data.
738     *
739     * @copyright 2011-2016  Chris Jean
740     * @author Chris Jean
741     * @license GNU General Public License v2.0
742     * @source https://github.com/chrisbliss18/php-ico
743     */
744    protected static function getIcoData($images)
745    {
746        if (!is_array($images) || empty($images))
747            return false;
748        $data = pack('vvv', 0, 1, count($images));
749        $pixel_data = '';
750        $icon_dir_entry_size = 16;
751        $offset = 6 + ($icon_dir_entry_size * count($images));
752        foreach ($images as $image) {
753            $data .= pack('CCCCvvVV', $image['width'], $image['height'], $image['color_palette_colors'], 0, 1, $image['bits_per_pixel'], $image['size'], $offset);
754            $pixel_data .= $image['data'];
755            $offset += $image['size'];
756        }
757        $data .= $pixel_data;
758        unset($pixel_data);
759        return $data;
760    }
761
762    /**
763     * Take a GD image resource and change it into a raw BMP format.
764     *
765     * @copyright 2011-2016  Chris Jean
766     * @author Chris Jean
767     * @license GNU General Public License v2.0
768     * @source https://github.com/chrisbliss18/php-ico
769     */
770    protected static function addFaviconImageData($im, &$images)
771    {
772        $width = imagesx($im);
773        $height = imagesy($im);
774        $pixel_data = [];
775        $opacity_data = [];
776        $current_opacity_val = 0;
777        for ($y = $height - 1; $y >= 0; $y--) {
778            for ($x = 0; $x < $width; $x++) {
779                $color = imagecolorat($im, $x, $y);
780                $alpha = ($color & 0x7F000000) >> 24;
781                $alpha = (1 - ($alpha / 127)) * 255;
782                $color &= 0xFFFFFF;
783                $color |= 0xFF000000 & ($alpha << 24);
784                $pixel_data[] = $color;
785                $opacity = ($alpha <= 127) ? 1 : 0;
786                $current_opacity_val = ($current_opacity_val << 1) | $opacity;
787                if ((($x + 1) % 32) == 0) {
788                    $opacity_data[] = $current_opacity_val;
789                    $current_opacity_val = 0;
790                }
791            }
792            if (($x % 32) > 0) {
793                while (($x++ % 32) > 0) {
794                    $current_opacity_val = $current_opacity_val << 1;
795                }
796                $opacity_data[] = $current_opacity_val;
797                $current_opacity_val = 0;
798            }
799        }
800        $image_header_size = 40;
801        $color_mask_size = $width * $height * 4;
802        $opacity_mask_size = (ceil($width / 32) * 4) * $height;
803        $data = pack('VVVvvVVVVVV', 40, $width, ($height * 2), 1, 32, 0, 0, 0, 0, 0, 0);
804        foreach ($pixel_data as $color) {
805            $data .= pack('V', $color);
806        }
807        foreach ($opacity_data as $opacity) {
808            $data .= pack('N', $opacity);
809        }
810        $image = [
811            'width'                => $width,
812            'height'               => $height,
813            'color_palette_colors' => 0,
814            'bits_per_pixel'       => 32,
815            'size'                 => $image_header_size + $color_mask_size + $opacity_mask_size,
816            'data'                 => $data,
817        ];
818        $images[] = $image;
819    }
820
821    /**
822     * @param bool $checkAccept Check the accept header
823     *
824     * @return bool
825     *
826     * @since 1.0.4
827     */
828    public static function webpSupport($checkAccept = false)
829    {
830        static $supported = null;
831        if ($supported === null) {
832            $config = Context::getContext()->theme->getConfiguration();
833
834            try {
835                $supported = Configuration::get('TB_USE_WEBP')
836                    && !empty($config['webp'])
837                    && function_exists('imagewebp');
838            } catch (PrestaShopException $e) {
839                $supported = false;
840            }
841        }
842
843        if ($checkAccept) {
844            $supported &= !empty($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'image/webp' ) !== false;
845        }
846
847        return $supported;
848    }
849
850    /**
851     * @return bool
852     *
853     * @since 1.0.4
854     */
855    public static function retinaSupport()
856    {
857        static $supported = null;
858        if ($supported === null) {
859            try {
860                $supported = (bool) Configuration::get('PS_HIGHT_DPI');
861            } catch (PrestaShopException $e) {
862                $supported = false;
863            }
864        }
865
866        return $supported;
867    }
868}
869