Completed
Push — refactor ( 888b68...dbf3dc )
by Paul
02:06
created

Ico::loadFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.009

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
394
                                imagesetpixel($im, $j, $i, $palette[$color['High']]);
395
                            }
396
                            $leftbits = false;
397 View Code Duplication
                        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
398
                            if ($maskBits[$maskoffset++] == 0) {
399
                                imagesetpixel($im, $j, $i, $palette[$color['Low']]);
400
                            }
401
                            ++$offset;
402
                            $leftbits = true;
403
                        }
404
                    }
405
                }
406
                break;
407
            case 1:
408
                /**
409
                 * 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ].
410
                 **/
411
                $colorbits = '';
412
                $total = strlen($this->formats[$index]['data']);
413
                for ($i = 0; $i < $total; ++$i) {
414
                    $colorbits .= str_pad(decbin(ord($this->formats[$index]['data'][$i])), 8, '0', STR_PAD_LEFT);
415
                }
416
417
                $offset = 0;
418
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
419
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
420
                        if ($maskBits[$offset] == 0) {
421
                            imagesetpixel($im, $j, $i, $palette[$colorbits[$offset]]);
422
                        }
423
                        ++$offset;
424
                    }
425
                }
426
                break;
427 1
        }
428
429 1
        return $im;
430
    }
431
432
    /**
433
     * Ico::AllocateColor()
434
     * Allocate a color on $im resource. This function prevents
435
     * from allocating same colors on the same pallete. Instead
436
     * if it finds that the color is already allocated, it only
437
     * returns the index to that color.
438
     * It supports alpha channel.
439
     *
440
     * @param resource $im     Image resource
441
     * @param int      $red    Red component
442
     * @param int      $green  Green component
443
     * @param int      $blue   Blue component
444
     * @param int      $alpha Alpha channel
445
     *
446
     * @return int Color index
447
     **/
448 1
    private function allocateColor(&$im, $red, $green, $blue, $alpha = 0)
449
    {
450 1
        $c = imagecolorexactalpha($im, $red, $green, $blue, $alpha);
451 1
        if ($c >= 0) {
452 1
            return $c;
453
        }
454
455
        return imagecolorallocatealpha($im, $red, $green, $blue, $alpha);
456
    }
457
}
458