xref: /plugin/farmer/vendor/splitbrain/php-ico/src/PhpIco.php (revision c609f1dcc91a56df760d51ba92f6e25b7289002c)
1<?php
2
3/*
4Copyright 2011-2016 Chris Jean & iThemes
5Licensed under GPLv2 or above
6*/
7
8namespace splitbrain\phpico;
9
10class PhpIco
11{
12    /**
13     * Images in the BMP format.
14     *
15     * @var array
16     */
17    private $images = array();
18
19    /**
20     * Constructor - Create a new ICO generator.
21     *
22     * If the constructor is not passed a file, a file will need to be supplied using the {@link PHP_ICO::add_image}
23     * function in order to generate an ICO file.
24     *
25     * @param string $file Optional. Path to the source image file.
26     * @param array $sizes Optional. An array of sizes (each size is an array with a width and height)
27     *                               that the source image should be rendered at in the generated ICO file.
28     *                               If sizes are not supplied, the size of the source image will be used.
29     */
30    public function __construct($file = false, $sizes = array())
31    {
32        $required_functions = array(
33            'getimagesize',
34            'imagecreatefromstring',
35            'imagecreatetruecolor',
36            'imagecolortransparent',
37            'imagecolorallocatealpha',
38            'imagealphablending',
39            'imagesavealpha',
40            'imagesx',
41            'imagesy',
42            'imagecopyresampled',
43        );
44
45        foreach ($required_functions as $function) {
46            if (!function_exists($function)) {
47                throw new \RuntimeException(
48                    "The PhpIco class was unable to find the $function function, which is part of the GD library. ".
49                    'Ensure that the system has the GD library installed and that PHP has access to it through a PHP '.
50                    'interface, such as PHP\'s GD module. Since this function was not found, the library will be '.
51                    'unable to create ICO files.'
52                );
53            }
54        }
55
56        if ($file) {
57            $this->addImage($file, $sizes);
58        }
59    }
60
61    /**
62     * Add an image to the generator.
63     *
64     * This function adds a source image to the generator. It serves two main purposes: add a source image if one was
65     * not supplied to the constructor and to add additional source images so that different images can be supplied for
66     * different sized images in the resulting ICO file. For instance, a small source image can be used for the small
67     * resolutions while a larger source image can be used for large resolutions.
68     *
69     * @param string $file Path to the source image file.
70     * @param array $sizes Optional. An array of sizes (each size is an array with a width and height) that the source
71     *                     image should be rendered at in the generated ICO file. If sizes are not supplied, the size
72     *                     of the source image will be used.
73     * @return boolean true on success and false on failure.
74     */
75    public function addImage($file, $sizes = array())
76    {
77        if (false === ($im = $this->loadImageFile($file))) {
78            return false;
79        }
80
81
82        if (empty($sizes)) {
83            $sizes = array(imagesx($im), imagesy($im));
84        }
85
86        // If just a single size was passed, put it in array.
87        if (!is_array($sizes[0])) {
88            $sizes = array($sizes);
89        }
90
91        foreach ((array)$sizes as $size) {
92            list($width, $height) = $size;
93
94            $new_im = imagecreatetruecolor($width, $height);
95
96            imagecolortransparent($new_im, imagecolorallocatealpha($new_im, 0, 0, 0, 127));
97            imagealphablending($new_im, false);
98            imagesavealpha($new_im, true);
99
100            $source_width = imagesx($im);
101            $source_height = imagesy($im);
102
103            if (false === imagecopyresampled(
104                    $new_im,
105                    $im,
106                    0,
107                    0,
108                    0,
109                    0,
110                    $width,
111                    $height,
112                    $source_width,
113                    $source_height)
114            ) {
115                continue;
116            }
117
118            $this->addImageData($new_im);
119        }
120
121        return true;
122    }
123
124    /**
125     * Write the ICO file data to a file path.
126     *
127     * @param string $file Path to save the ICO file data into.
128     * @return boolean true on success and false on failure.
129     */
130    public function saveIco($file)
131    {
132        if (false === ($data = $this->getIcoData())) {
133            return false;
134        }
135
136        if (false === ($fh = fopen($file, 'w'))) {
137            return false;
138        }
139
140        if (false === (fwrite($fh, $data))) {
141            fclose($fh);
142            return false;
143        }
144
145        fclose($fh);
146
147        return true;
148    }
149
150    /**
151     * Generate the final ICO data by creating a file header and adding the image data.
152     */
153    protected function getIcoData()
154    {
155        if (!is_array($this->images) || empty($this->images)) {
156            return false;
157        }
158
159
160        $data = pack('vvv', 0, 1, count($this->images));
161        $pixel_data = '';
162
163        $icon_dir_entry_size = 16;
164
165        $offset = 6 + ($icon_dir_entry_size * count($this->images));
166
167        foreach ($this->images as $image) {
168            $data .= pack(
169                'CCCCvvVV',
170                $image['width'],
171                $image['height'],
172                $image['color_palette_colors'],
173                0,
174                1,
175                $image['bits_per_pixel'],
176                $image['size'],
177                $offset
178            );
179            $pixel_data .= $image['data'];
180
181            $offset += $image['size'];
182        }
183
184        $data .= $pixel_data;
185        unset($pixel_data);
186
187
188        return $data;
189    }
190
191    /**
192     * Take a GD image resource and change it into a raw BMP format.
193     */
194    protected function addImageData($im)
195    {
196        $width = imagesx($im);
197        $height = imagesy($im);
198
199
200        $pixel_data = array();
201
202        $opacity_data = array();
203        $current_opacity_val = 0;
204
205        for ($y = $height - 1; $y >= 0; $y--) {
206            for ($x = 0; $x < $width; $x++) {
207                $color = imagecolorat($im, $x, $y);
208
209                $alpha = ($color & 0x7F000000) >> 24;
210                $alpha = (1 - ($alpha / 127)) * 255;
211
212                $color &= 0xFFFFFF;
213                $color |= 0xFF000000 & ($alpha << 24);
214
215                $pixel_data[] = $color;
216
217
218                $opacity = ($alpha <= 127) ? 1 : 0;
219
220                $current_opacity_val = ($current_opacity_val << 1) | $opacity;
221
222                if ((($x + 1) % 32) == 0) {
223                    $opacity_data[] = $current_opacity_val;
224                    $current_opacity_val = 0;
225                }
226            }
227
228            if (($x % 32) > 0) {
229                while (($x++ % 32) > 0) {
230                    $current_opacity_val = $current_opacity_val << 1;
231                }
232
233                $opacity_data[] = $current_opacity_val;
234                $current_opacity_val = 0;
235            }
236        }
237
238        $image_header_size = 40;
239        $color_mask_size = $width * $height * 4;
240        $opacity_mask_size = (ceil($width / 32) * 4) * $height;
241
242
243        $data = pack('VVVvvVVVVVV', 40, $width, ($height * 2), 1, 32, 0, 0, 0, 0, 0, 0);
244
245        foreach ($pixel_data as $color) {
246            $data .= pack('V', $color);
247        }
248
249        foreach ($opacity_data as $opacity) {
250            $data .= pack('N', $opacity);
251        }
252
253
254        $image = array(
255            'width' => $width,
256            'height' => $height,
257            'color_palette_colors' => 0,
258            'bits_per_pixel' => 32,
259            'size' => $image_header_size + $color_mask_size + $opacity_mask_size,
260            'data' => $data,
261        );
262
263        $this->images[] = $image;
264    }
265
266    /**
267     * Read in the source image file and convert it into a GD image resource.
268     */
269    protected function loadImageFile($file)
270    {
271        if (!is_string($file) || empty($file)) {
272            return false;
273        }
274        // Run a cheap check to verify that it is an image file.
275        if (false === ($size = getimagesize($file))) {
276            return false;
277        }
278
279        if (false === ($file_data = file_get_contents($file))) {
280            return false;
281        }
282
283        if (false === ($im = imagecreatefromstring($file_data))) {
284            return false;
285        }
286
287        unset($file_data);
288
289
290        return $im;
291    }
292}
293