Completed
Push — refactor ( dbf3dc...4a4a60 )
by Paul
02:05
created

Ico::getImage()   D

Complexity

Conditions 31
Paths 61

Size

Total Lines 162
Code Lines 101

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 82
CRAP Score 75.6126

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 162
ccs 82
cts 128
cp 0.6405
rs 4.3983
cc 31
eloc 101
nc 61
nop 1
crap 75.6126

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 2
    public function __construct($path = '')
41
    {
42 2
        if (strlen($path) > 0) {
43
            $this->loadFile($path);
44
        }
45 2
    }
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 2
    public function loadFile($path)
56
    {
57 2
        $this->filename = $path;
58 2
        if (($fp = @fopen($path, 'rb')) !== false) {
59 2
            $data = '';
60 2
            while (!feof($fp)) {
61 2
                $data .= fread($fp, 4096);
62 2
            }
63 2
            fclose($fp);
64
65 2
            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 2
    private function loadData($data)
81
    {
82 2
        $this->formats = [];
83
84
        /**
85
         * ICO header.
86
         **/
87 2
        $icodata = unpack('SReserved/SType/SCount', $data);
88 2
        $this->ico = $icodata;
89 2
        $data = substr($data, 6);
90
91
        /*
92
         * Extract each icon header
93
         **/
94 2
        for ($i = 0; $i < $this->ico['Count']; ++$i) {
95 2
            $icodata = unpack('CWidth/CHeight/CColorCount/CReserved/SPlanes/SBitCount/LSizeInBytes/LFileOffset', $data);
96 2
            $icodata['FileOffset'] -= ($this->ico['Count'] * 16) + 6;
97 2
            if ($icodata['ColorCount'] == 0) {
98 1
                $icodata['ColorCount'] = 256;
99 1
            }
100 2
            $this->formats[] = $icodata;
101
102 2
            $data = substr($data, 16);
103 2
        }
104
105
        /*
106
         * Extract aditional headers for each extracted icon header
107
         **/
108 2
        $formatCount=count($this->formats);
109 2
        for ($i = 0; $i < $formatCount; ++$i) {
110 2
            $icodata = unpack(
111
                'LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/'.
112 2
                'LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant',
113 2
                substr($data, $this->formats[$i]['FileOffset'])
114 2
            );
115
116 2
            $this->formats[$i]['header'] = $icodata;
117 2
            $this->formats[$i]['colors'] = [];
118
119 2
            $this->formats[$i]['BitCount'] = $this->formats[$i]['header']['BitCount'];
120
121 2
            switch ($this->formats[$i]['BitCount']) {
122 2
                case 32:
123 2
                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 1
                case 8:
134 1
                case 4:
135 1
                    $icodata = substr(
136 1
                        $data,
137 1
                        $this->formats[$i]['FileOffset'] + $icodata['Size'],
138 1
                        $this->formats[$i]['ColorCount'] * 4
139 1
                    );
140 1
                    $offset = 0;
141 1
                    for ($j = 0; $j < $this->formats[$i]['ColorCount']; ++$j) {
142 1
                        $this->formats[$i]['colors'][] = [
143 1
                            'blue' => ord($icodata[$offset]),
144 1
                            'green' => ord($icodata[$offset + 1]),
145 1
                            'red' => ord($icodata[$offset + 2]),
146 1
                            'reserved' => ord($icodata[$offset + 3]),
147
                        ];
148 1
                        $offset += 4;
149 1
                    }
150 1
                    $length = $this->formats[$i]['header']['Width'] *
151 1
                        $this->formats[$i]['header']['Height'] *
152 1
                        (1 + $this->formats[$i]['BitCount']) / $this->formats[$i]['BitCount'];
153 1
                    $this->formats[$i]['data'] = substr(
154 1
                        $data,
155 1
                        $this->formats[$i]['FileOffset'] +
156 1
                            ($this->formats[$i]['ColorCount'] * 4) +
157 1
                            $this->formats[$i]['header']['Size'],
158
                        $length
159 1
                    );
160 1
                    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 2
            }
189 2
            $this->formats[$i]['data_length'] = strlen($this->formats[$i]['data']);
190 2
        }
191
192 2
        return true;
193
    }
194
195
    /**
196
     * Return the total icons extracted at the moment.
197
     *
198
     * @return int Total icons
199
     **/
200 2
    public function getTotalIcons()
201
    {
202 2
        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 2
    public function getIconInfo($index)
214
    {
215 2
        if (isset($this->formats[$index])) {
216 2
            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 2
    public function setBackground($red = 255, $green = 255, $blue = 255)
232
    {
233 2
        if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) {
234 2
            $green = hexdec($red[3].$red[4]);
235 2
            $blue = hexdec($red[5].$red[6]);
236 2
            $red = hexdec($red[1].$red[2]);
237 2
        }
238
239 2
        $this->bgcolor = [$red, $green, $blue];
240 2
    }
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 2
    public function getImage($index)
263
    {
264 2
        if (!isset($this->formats[$index])) {
265
            return false;
266
        }
267
268
        // create image filled with desired background color
269 2
        $im = imagecreatetruecolor($this->formats[$index]['Width'], $this->formats[$index]['Height']);
270 2
        $bgcolor = $this->allocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]);
271 2
        imagefilledrectangle($im, 0, 0, $this->formats[$index]['Width'], $this->formats[$index]['Height'], $bgcolor);
272
273 2
        if ($this->bgcolorTransparent) {
274
            imagecolortransparent($im, $bgcolor);
275
        }
276
277
        //we may build a string of 1/0 to represent the XOR mask
278 2
        $maskBits='';
279
        //we may build a palette for 8 bit images
280 2
        $palette=[];
281
282
        // allocate palette and get XOR image
283 2
        if (in_array($this->formats[$index]['BitCount'], [1, 4, 8, 24])) {
284 1
            if ($this->formats[$index]['BitCount'] != 24) {
285 1
                $palette = [];
286 1
                for ($i = 0; $i < $this->formats[$index]['ColorCount']; ++$i) {
287 1
                    $palette[$i] = $this->allocateColor(
288 1
                        $im,
289 1
                        $this->formats[$index]['colors'][$i]['red'],
290 1
                        $this->formats[$index]['colors'][$i]['green'],
291 1
                        $this->formats[$index]['colors'][$i]['blue'],
292 1
                        round($this->formats[$index]['colors'][$i]['reserved'] / 255 * 127)
293 1
                    );
294 1
                }
295 1
            }
296
297
            // build XOR mask bits
298 1
            $width = $this->formats[$index]['Width'];
299 1
            if (($width % 32) > 0) {
300
                $width += (32 - ($this->formats[$index]['Width'] % 32));
301
            }
302 1
            $offset = $this->formats[$index]['Width'] *
303 1
                $this->formats[$index]['Height'] *
304 1
                $this->formats[$index]['BitCount'] / 8;
305 1
            $total_bytes = ($width * $this->formats[$index]['Height']) / 8;
306 1
            $maskBits = '';
307 1
            $bytes = 0;
308 1
            $bytes_per_line = ($this->formats[$index]['Width'] / 8);
309 1
            $bytes_to_remove = (($width - $this->formats[$index]['Width']) / 8);
310 1
            for ($i = 0; $i < $total_bytes; ++$i) {
311 1
                $maskBits .= str_pad(decbin(ord($this->formats[$index]['data'][$offset + $i])), 8, '0', STR_PAD_LEFT);
312 1
                ++$bytes;
313 1
                if ($bytes == $bytes_per_line) {
314 1
                    $i += $bytes_to_remove;
315 1
                    $bytes = 0;
316 1
                }
317 1
            }
318 1
        }
319
320
        // now paint pixels based on bit count
321 2
        switch ($this->formats[$index]['BitCount']) {
322 2
            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 1
            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 1
            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 1
            case 4:
378
                /**
379
                 * 4 bits: half byte/nibble per pixel [ COLOR INDEX ].
380
                 **/
381 1
                $offset = 0;
382 1
                $maskoffset = 0;
383 1
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
384 1
                    for ($j = 0; $j < $this->formats[$index]['Width']; $j+=2) {
385 1
                        $colorByte = ord($this->formats[$index]['data'][$offset]);
386 1
                        $lowNibble=$colorByte&0x0f;
387 1
                        $highNibble=($colorByte&0xf0) >> 4;
388
389 1
                        if ($maskBits[$maskoffset++] == 0) {
390 1
                            imagesetpixel($im, $j, $i, $palette[$highNibble]);
391 1
                        }
392
393 1
                        if ($maskBits[$maskoffset++] == 0) {
394 1
                            imagesetpixel($im, $j+1, $i, $palette[$lowNibble]);
395 1
                        }
396 1
                        $offset++;
397 1
                    }
398 1
                }
399 1
                break;
400
            case 1:
401
                /**
402
                 * 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ].
403
                 **/
404
                $colorbits = '';
405
                $total = strlen($this->formats[$index]['data']);
406
                for ($i = 0; $i < $total; ++$i) {
407
                    $colorbits .= str_pad(decbin(ord($this->formats[$index]['data'][$i])), 8, '0', STR_PAD_LEFT);
408
                }
409
410
                $offset = 0;
411
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
412
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
413
                        if ($maskBits[$offset] == 0) {
414
                            imagesetpixel($im, $j, $i, $palette[$colorbits[$offset]]);
415
                        }
416
                        ++$offset;
417
                    }
418
                }
419
                break;
420 2
        }
421
422 2
        return $im;
423
    }
424
425
    /**
426
     * Ico::AllocateColor()
427
     * Allocate a color on $im resource. This function prevents
428
     * from allocating same colors on the same pallete. Instead
429
     * if it finds that the color is already allocated, it only
430
     * returns the index to that color.
431
     * It supports alpha channel.
432
     *
433
     * @param resource $im     Image resource
434
     * @param int      $red    Red component
435
     * @param int      $green  Green component
436
     * @param int      $blue   Blue component
437
     * @param int      $alpha Alpha channel
438
     *
439
     * @return int Color index
440
     **/
441 2
    private function allocateColor(&$im, $red, $green, $blue, $alpha = 0)
442
    {
443 2
        $c = imagecolorexactalpha($im, $red, $green, $blue, $alpha);
444 2
        if ($c >= 0) {
445 2
            return $c;
446
        }
447
448
        return imagecolorallocatealpha($im, $red, $green, $blue, $alpha);
449
    }
450
}
451