Completed
Branch master (99fd1f)
by Paul
03:48
created

Ico::loadFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
ccs 8
cts 9
cp 0.8889
rs 9.4285
cc 3
eloc 9
nc 3
nop 1
crap 3.0123
1
<?php
2
3
namespace Elphin\IcoFileLoader;
4
5
/**
6
 * Open ICO files and extract any size/depth to PNG format.
7
 */
8
class Ico
9
{
10
    /**
11
     * Background color on icon extraction.
12
     * @var array(R, G, B) = array(255, 255, 255)
13
     */
14
    public $bgcolor = [255, 255, 255];
15
16
    /**
17
     * @var bool Is background color transparent?
18
     */
19
    public $bgcolorTransparent = false;
20
21
    private $filename;
22
    private $ico;
23
    private $formats;
24
25
    /**
26
     * Constructor
27
     *
28
     * @param string $path optional path to ICO file
29
     */
30 5
    public function __construct($path = '')
31
    {
32 5
        if (strlen($path) > 0) {
33 4
            $this->loadFile($path);
34
        }
35 5
    }
36
37
    /**
38
     * Load an ICO file (don't need to call this is if fill the
39
     * parameter in the class constructor).
40
     *
41
     * @param string $path Path to ICO file
42
     *
43
     * @return bool Success
44
     */
45 5
    public function loadFile($path)
46
    {
47 5
        $this->filename = $path;
48 5
        if (($fp = @fopen($path, 'rb')) !== false) {
49 5
            $data = '';
50 5
            while (!feof($fp)) {
51 5
                $data .= fread($fp, 4096);
52
            }
53 5
            fclose($fp);
54
55 5
            return $this->loadData($data);
56
        }
57
58
        return false;
59
    }
60
61
    /**
62
     * Load an ICO data. If you prefer to open the file
63
     * and return the binary data you can use this function
64
     * directly. Otherwise use loadFile() instead.
65
     *
66
     * @param string $data Binary data of ICO file
67
     *
68
     * @return bool Success
69
     */
70 5
    private function loadData($data)
71
    {
72 5
        $this->formats = [];
73
74
        /**
75
         * ICO header.
76
         **/
77 5
        $icodata = unpack('SReserved/SType/SCount', $data);
78 5
        $this->ico = $icodata;
79 5
        $data = substr($data, 6);
80
81
        /*
82
         * Extract each icon header
83
         **/
84 5
        for ($i = 0; $i < $this->ico['Count']; ++$i) {
85 5
            $icodata = unpack('CWidth/CHeight/CColorCount/CReserved/SPlanes/SBitCount/LSizeInBytes/LFileOffset', $data);
86 5
            $icodata['FileOffset'] -= ($this->ico['Count'] * 16) + 6;
87 5
            if ($icodata['ColorCount'] == 0) {
88 3
                $icodata['ColorCount'] = 256;
89
            }
90 5
            $this->formats[] = $icodata;
91
92 5
            $data = substr($data, 16);
93
        }
94
95
        /*
96
         * Extract aditional headers for each extracted icon header
97
         **/
98 5
        $formatCount = count($this->formats);
99 5
        for ($i = 0; $i < $formatCount; ++$i) {
100 5
            $icodata = unpack(
101
                'LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/' .
102 5
                'LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant',
103 5
                substr($data, $this->formats[$i]['FileOffset'])
104
            );
105
106 5
            $this->formats[$i]['header'] = $icodata;
107 5
            $this->formats[$i]['colors'] = [];
108
109 5
            $this->formats[$i]['BitCount'] = $this->formats[$i]['header']['BitCount'];
110
111 5
            switch ($this->formats[$i]['BitCount']) {
112 5
                case 32:
113 4
                case 24:
114 2
                    $length = $this->formats[$i]['header']['Width'] *
115 2
                        $this->formats[$i]['header']['Height'] *
116 2
                        ($this->formats[$i]['BitCount'] / 8);
117 2
                    $this->formats[$i]['data'] = substr(
118
                        $data,
119 2
                        $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'],
120
                        $length
121
                    );
122 2
                    break;
123 3
                case 8:
124 3
                case 4:
125 2
                    $icodata = substr(
126
                        $data,
127 2
                        $this->formats[$i]['FileOffset'] + $icodata['Size'],
128 2
                        $this->formats[$i]['ColorCount'] * 4
129
                    );
130 2
                    $offset = 0;
131 2
                    for ($j = 0; $j < $this->formats[$i]['ColorCount']; ++$j) {
132 2
                        $this->formats[$i]['colors'][] = [
133 2
                            'blue' => ord($icodata[$offset]),
134 2
                            'green' => ord($icodata[$offset + 1]),
135 2
                            'red' => ord($icodata[$offset + 2]),
136 2
                            'reserved' => ord($icodata[$offset + 3]),
137
                        ];
138 2
                        $offset += 4;
139
                    }
140 2
                    $length = $this->formats[$i]['header']['Width'] *
141 2
                        $this->formats[$i]['header']['Height'] *
142 2
                        (1 + $this->formats[$i]['BitCount']) / $this->formats[$i]['BitCount'];
143 2
                    $this->formats[$i]['data'] = substr(
144
                        $data,
145 2
                        $this->formats[$i]['FileOffset'] +
146 2
                        ($this->formats[$i]['ColorCount'] * 4) +
147 2
                        $this->formats[$i]['header']['Size'],
148
                        $length
149
                    );
150 2
                    break;
151 1
                case 1:
152 1
                    $icodata = substr(
153
                        $data,
154 1
                        $this->formats[$i]['FileOffset'] + $icodata['Size'],
155 1
                        $this->formats[$i]['ColorCount'] * 4
156
                    );
157
158 1
                    $this->formats[$i]['colors'][] = [
159 1
                        'blue' => ord($icodata[0]),
160 1
                        'green' => ord($icodata[1]),
161 1
                        'red' => ord($icodata[2]),
162 1
                        'reserved' => ord($icodata[3]),
163
                    ];
164 1
                    $this->formats[$i]['colors'][] = [
165 1
                        'blue' => ord($icodata[4]),
166 1
                        'green' => ord($icodata[5]),
167 1
                        'red' => ord($icodata[6]),
168 1
                        'reserved' => ord($icodata[7]),
169
                    ];
170
171 1
                    $length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] / 8;
172 1
                    $this->formats[$i]['data'] = substr(
173
                        $data,
174 1
                        $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'] + 8,
175
                        $length
176
                    );
177 1
                    break;
178
            }
179 5
            $this->formats[$i]['data_length'] = strlen($this->formats[$i]['data']);
180
        }
181
182 5
        return true;
183
    }
184
185
    /**
186
     * Return the total icons extracted at the moment.
187
     *
188
     * @return int Total icons
189
     */
190 5
    public function getTotalIcons()
191
    {
192 5
        return count($this->formats);
193
    }
194
195
    /**
196
     * Return the icon header corresponding to that index.
197
     *
198
     * @param int $index Icon index
199
     *
200
     * @return resource|bool Icon header or false
201
     */
202 5
    public function getIconInfo($index)
203
    {
204 5
        if (isset($this->formats[$index])) {
205 5
            return $this->formats[$index];
206
        }
207
208 1
        return false;
209
    }
210
211
    /**
212
     * Changes background color of extraction. You can set
213
     * the 3 color components or set $red = '#xxxxxx' (HTML format)
214
     * and leave all other blanks.
215
     *
216
     * @param int $red Red component
217
     * @param int $green Green component
218
     * @param int $blue Blue component
219
     */
220 5
    public function setBackground($red = 255, $green = 255, $blue = 255)
221
    {
222 5
        if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) {
223 5
            $green = hexdec($red[3] . $red[4]);
224 5
            $blue = hexdec($red[5] . $red[6]);
225 5
            $red = hexdec($red[1] . $red[2]);
226
        }
227
228 5
        $this->bgcolor = [$red, $green, $blue];
229 5
    }
230
231
    /**
232
     * Set background color to be saved as transparent.
233
     *
234
     * @param bool $transparent Is Transparent or not
235
     *
236
     * @return bool Is Transparent or not
237
     */
238
    public function setBackgroundTransparent($transparent = true)
239
    {
240
        return $this->bgcolorTransparent = $transparent;
241
    }
242
243
    /**
244
     * Return an image resource with the icon stored
245
     * on the $index position of the ICO file.
246
     *
247
     * @param int $index Position of the icon inside ICO
248
     *
249
     * @return resource|bool Image resource
250
     **/
251 5
    public function getImage($index)
252
    {
253 5
        if (!isset($this->formats[$index])) {
254
            return false;
255
        }
256
257
        // create image filled with desired background color
258 5
        $im = imagecreatetruecolor($this->formats[$index]['Width'], $this->formats[$index]['Height']);
259 5
        $bgcolor = $this->allocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]);
260 5
        imagefilledrectangle($im, 0, 0, $this->formats[$index]['Width'], $this->formats[$index]['Height'], $bgcolor);
261
262 5
        if ($this->bgcolorTransparent) {
263
            imagecolortransparent($im, $bgcolor);
264
        }
265
266
        //we may build a string of 1/0 to represent the XOR mask
267 5
        $maskBits = '';
268
        //we may build a palette for 8 bit images
269 5
        $palette = [];
270
271
        // allocate palette and get XOR image
272 5
        if (in_array($this->formats[$index]['BitCount'], [1, 4, 8, 24])) {
273 4
            if ($this->formats[$index]['BitCount'] != 24) {
274 3
                $palette = [];
275 3
                for ($i = 0; $i < $this->formats[$index]['ColorCount']; ++$i) {
276 3
                    $palette[$i] = $this->allocateColor(
277
                        $im,
278 3
                        $this->formats[$index]['colors'][$i]['red'],
279 3
                        $this->formats[$index]['colors'][$i]['green'],
280 3
                        $this->formats[$index]['colors'][$i]['blue'],
281 3
                        round($this->formats[$index]['colors'][$i]['reserved'] / 255 * 127)
282
                    );
283
                }
284
            }
285
286
            // build XOR mask bits
287 4
            $width = $this->formats[$index]['Width'];
288 4
            if (($width % 32) > 0) {
289
                $width += (32 - ($this->formats[$index]['Width'] % 32));
290
            }
291 4
            $offset = $this->formats[$index]['Width'] *
292 4
                $this->formats[$index]['Height'] *
293 4
                $this->formats[$index]['BitCount'] / 8;
294 4
            $total_bytes = ($width * $this->formats[$index]['Height']) / 8;
295 4
            $maskBits = '';
296 4
            $bytes = 0;
297 4
            $bytes_per_line = ($this->formats[$index]['Width'] / 8);
298 4
            $bytes_to_remove = (($width - $this->formats[$index]['Width']) / 8);
299 4
            for ($i = 0; $i < $total_bytes; ++$i) {
300 4
                $maskBits .= str_pad(decbin(ord($this->formats[$index]['data'][$offset + $i])), 8, '0', STR_PAD_LEFT);
301 4
                ++$bytes;
302 4
                if ($bytes == $bytes_per_line) {
303 4
                    $i += $bytes_to_remove;
304 4
                    $bytes = 0;
305
                }
306
            }
307
        }
308
309
        // now paint pixels based on bit count
310 5
        switch ($this->formats[$index]['BitCount']) {
311 5
            case 32:
312
                /**
313
                 * 32 bits: 4 bytes per pixel [ B | G | R | ALPHA ].
314
                 **/
315 1
                $offset = 0;
316 1
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
317 1
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
318 1
                        $color = substr($this->formats[$index]['data'], $offset, 4);
319 1
                        if (ord($color[3]) > 0) {
320 1
                            $palette = $this->allocateColor(
321
                                $im,
322 1
                                ord($color[2]),
323 1
                                ord($color[1]),
324 1
                                ord($color[0]),
325 1
                                127 - round(ord($color[3]) / 255 * 127)
326
                            );
327 1
                            imagesetpixel($im, $j, $i, $palette);
328
                        }
329 1
                        $offset += 4;
330
                    }
331
                }
332 1
                break;
333 4
            case 24:
334
                /**
335
                 * 24 bits: 3 bytes per pixel [ B | G | R ].
336
                 **/
337 1
                $offset = 0;
338 1
                $bitoffset = 0;
339 1
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
340 1
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
341 1
                        if ($maskBits[$bitoffset] == 0) {
342 1
                            $color = substr($this->formats[$index]['data'], $offset, 3);
343 1
                            $palette = $this->allocateColor($im, ord($color[2]), ord($color[1]), ord($color[0]));
344 1
                            imagesetpixel($im, $j, $i, $palette);
345
                        }
346 1
                        $offset += 3;
347 1
                        ++$bitoffset;
348
                    }
349
                }
350 1
                break;
351 3
            case 8:
352
                /**
353
                 * 8 bits: 1 byte per pixel [ COLOR INDEX ].
354
                 **/
355 1
                $offset = 0;
356 1
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
357 1
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
358 1
                        if ($maskBits[$offset] == 0) {
359 1
                            $color = ord(substr($this->formats[$index]['data'], $offset, 1));
360 1
                            imagesetpixel($im, $j, $i, $palette[$color]);
361
                        }
362 1
                        ++$offset;
363
                    }
364
                }
365 1
                break;
366 2
            case 4:
367
                /**
368
                 * 4 bits: half byte/nibble per pixel [ COLOR INDEX ].
369
                 **/
370 1
                $offset = 0;
371 1
                $maskoffset = 0;
372 1
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
373 1
                    for ($j = 0; $j < $this->formats[$index]['Width']; $j += 2) {
374 1
                        $colorByte = ord($this->formats[$index]['data'][$offset]);
375 1
                        $lowNibble = $colorByte & 0x0f;
376 1
                        $highNibble = ($colorByte & 0xf0) >> 4;
377
378 1
                        if ($maskBits[$maskoffset++] == 0) {
379 1
                            imagesetpixel($im, $j, $i, $palette[$highNibble]);
380
                        }
381
382 1
                        if ($maskBits[$maskoffset++] == 0) {
383 1
                            imagesetpixel($im, $j + 1, $i, $palette[$lowNibble]);
384
                        }
385 1
                        $offset++;
386
                    }
387
                }
388 1
                break;
389 1
            case 1:
390
                /**
391
                 * 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ].
392
                 **/
393 1
                $colorbits = '';
394 1
                $total = strlen($this->formats[$index]['data']);
395 1
                for ($i = 0; $i < $total; ++$i) {
396 1
                    $colorbits .= str_pad(decbin(ord($this->formats[$index]['data'][$i])), 8, '0', STR_PAD_LEFT);
397
                }
398
399 1
                $offset = 0;
400 1
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
401 1
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
402 1
                        if ($maskBits[$offset] == 0) {
403 1
                            imagesetpixel($im, $j, $i, $palette[$colorbits[$offset]]);
404
                        }
405 1
                        ++$offset;
406
                    }
407
                }
408 1
                break;
409
        }
410
411 5
        return $im;
412
    }
413
414
    /**
415
     * Allocate a color on $im resource. This function prevents
416
     * from allocating same colors on the same pallete. Instead
417
     * if it finds that the color is already allocated, it only
418
     * returns the index to that color.
419
     * It supports alpha channel.
420
     *
421
     * @param resource $im Image resource
422
     * @param int $red Red component
423
     * @param int $green Green component
424
     * @param int $blue Blue component
425
     * @param int $alpha Alpha channel
426
     *
427
     * @return int Color index
428
     */
429 5
    private function allocateColor(&$im, $red, $green, $blue, $alpha = 0)
430
    {
431 5
        $c = imagecolorexactalpha($im, $red, $green, $blue, $alpha);
432 5
        if ($c >= 0) {
433 5
            return $c;
434
        }
435
436
        return imagecolorallocatealpha($im, $red, $green, $blue, $alpha);
437
    }
438
}
439