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