Completed
Push — refactor ( 56de83...8601e9 )
by Paul
03:03
created

Ico   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 438
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 81.2%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 55
c 2
b 1
f 0
lcom 1
cbo 0
dl 0
loc 438
ccs 203
cts 250
cp 0.812
rs 6.8

9 Methods

Rating   Name   Duplication   Size   Complexity  
A loadFile() 0 15 3
A getTotalIcons() 0 4 1
A getIconInfo() 0 8 2
A setBackground() 0 10 3
A setBackgroundTransparent() 0 4 1
D loadData() 0 114 10
A allocateColor() 0 9 2
A __construct() 0 6 2
D getImage() 0 162 31

How to fix   Complexity   

Complex Class

Complex classes like Ico often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Ico, and based on these observations, apply Extract Interface, too.

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 4
    public function __construct($path = '')
41
    {
42 4
        if (strlen($path) > 0) {
43 3
            $this->loadFile($path);
44 3
        }
45 4
    }
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 4
    public function loadFile($path)
56
    {
57 4
        $this->filename = $path;
58 4
        if (($fp = @fopen($path, 'rb')) !== false) {
59 4
            $data = '';
60 4
            while (!feof($fp)) {
61 4
                $data .= fread($fp, 4096);
62 4
            }
63 4
            fclose($fp);
64
65 4
            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 4
    private function loadData($data)
81
    {
82 4
        $this->formats = [];
83
84
        /**
85
         * ICO header.
86
         **/
87 4
        $icodata = unpack('SReserved/SType/SCount', $data);
88 4
        $this->ico = $icodata;
89 4
        $data = substr($data, 6);
90
91
        /*
92
         * Extract each icon header
93
         **/
94 4
        for ($i = 0; $i < $this->ico['Count']; ++$i) {
95 4
            $icodata = unpack('CWidth/CHeight/CColorCount/CReserved/SPlanes/SBitCount/LSizeInBytes/LFileOffset', $data);
96 4
            $icodata['FileOffset'] -= ($this->ico['Count'] * 16) + 6;
97 4
            if ($icodata['ColorCount'] == 0) {
98 3
                $icodata['ColorCount'] = 256;
99 3
            }
100 4
            $this->formats[] = $icodata;
101
102 4
            $data = substr($data, 16);
103 4
        }
104
105
        /*
106
         * Extract aditional headers for each extracted icon header
107
         **/
108 4
        $formatCount = count($this->formats);
109 4
        for ($i = 0; $i < $formatCount; ++$i) {
110 4
            $icodata = unpack(
111
                'LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/' .
112 4
                'LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant',
113 4
                substr($data, $this->formats[$i]['FileOffset'])
114 4
            );
115
116 4
            $this->formats[$i]['header'] = $icodata;
117 4
            $this->formats[$i]['colors'] = [];
118
119 4
            $this->formats[$i]['BitCount'] = $this->formats[$i]['header']['BitCount'];
120
121 4
            switch ($this->formats[$i]['BitCount']) {
122 4
                case 32:
123 4
                case 24:
124 2
                    $length = $this->formats[$i]['header']['Width'] *
125 2
                        $this->formats[$i]['header']['Height'] *
126 2
                        ($this->formats[$i]['BitCount'] / 8);
127 2
                    $this->formats[$i]['data'] = substr(
128 2
                        $data,
129 2
                        $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'],
130
                        $length
131 2
                    );
132 2
                    break;
133 2
                case 8:
134 2
                case 4:
135 2
                    $icodata = substr(
136 2
                        $data,
137 2
                        $this->formats[$i]['FileOffset'] + $icodata['Size'],
138 2
                        $this->formats[$i]['ColorCount'] * 4
139 2
                    );
140 2
                    $offset = 0;
141 2
                    for ($j = 0; $j < $this->formats[$i]['ColorCount']; ++$j) {
142 2
                        $this->formats[$i]['colors'][] = [
143 2
                            'blue' => ord($icodata[$offset]),
144 2
                            'green' => ord($icodata[$offset + 1]),
145 2
                            'red' => ord($icodata[$offset + 2]),
146 2
                            'reserved' => ord($icodata[$offset + 3]),
147
                        ];
148 2
                        $offset += 4;
149 2
                    }
150 2
                    $length = $this->formats[$i]['header']['Width'] *
151 2
                        $this->formats[$i]['header']['Height'] *
152 2
                        (1 + $this->formats[$i]['BitCount']) / $this->formats[$i]['BitCount'];
153 2
                    $this->formats[$i]['data'] = substr(
154 2
                        $data,
155 2
                        $this->formats[$i]['FileOffset'] +
156 2
                        ($this->formats[$i]['ColorCount'] * 4) +
157 2
                        $this->formats[$i]['header']['Size'],
158
                        $length
159 2
                    );
160 2
                    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 4
            }
189 4
            $this->formats[$i]['data_length'] = strlen($this->formats[$i]['data']);
190 4
        }
191
192 4
        return true;
193
    }
194
195
    /**
196
     * Return the total icons extracted at the moment.
197
     *
198
     * @return int Total icons
199
     **/
200 4
    public function getTotalIcons()
201
    {
202 4
        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 4
    public function getIconInfo($index)
214
    {
215 4
        if (isset($this->formats[$index])) {
216 4
            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 4
    public function setBackground($red = 255, $green = 255, $blue = 255)
232
    {
233 4
        if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) {
234 4
            $green = hexdec($red[3] . $red[4]);
235 4
            $blue = hexdec($red[5] . $red[6]);
236 4
            $red = hexdec($red[1] . $red[2]);
237 4
        }
238
239 4
        $this->bgcolor = [$red, $green, $blue];
240 4
    }
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 4
    public function getImage($index)
263
    {
264 4
        if (!isset($this->formats[$index])) {
265
            return false;
266
        }
267
268
        // create image filled with desired background color
269 4
        $im = imagecreatetruecolor($this->formats[$index]['Width'], $this->formats[$index]['Height']);
270 4
        $bgcolor = $this->allocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]);
271 4
        imagefilledrectangle($im, 0, 0, $this->formats[$index]['Width'], $this->formats[$index]['Height'], $bgcolor);
272
273 4
        if ($this->bgcolorTransparent) {
274
            imagecolortransparent($im, $bgcolor);
275
        }
276
277
        //we may build a string of 1/0 to represent the XOR mask
278 4
        $maskBits = '';
279
        //we may build a palette for 8 bit images
280 4
        $palette = [];
281
282
        // allocate palette and get XOR image
283 4
        if (in_array($this->formats[$index]['BitCount'], [1, 4, 8, 24])) {
284 3
            if ($this->formats[$index]['BitCount'] != 24) {
285 2
                $palette = [];
286 2
                for ($i = 0; $i < $this->formats[$index]['ColorCount']; ++$i) {
287 2
                    $palette[$i] = $this->allocateColor(
288 2
                        $im,
289 2
                        $this->formats[$index]['colors'][$i]['red'],
290 2
                        $this->formats[$index]['colors'][$i]['green'],
291 2
                        $this->formats[$index]['colors'][$i]['blue'],
292 2
                        round($this->formats[$index]['colors'][$i]['reserved'] / 255 * 127)
293 2
                    );
294 2
                }
295 2
            }
296
297
            // build XOR mask bits
298 3
            $width = $this->formats[$index]['Width'];
299 3
            if (($width % 32) > 0) {
300
                $width += (32 - ($this->formats[$index]['Width'] % 32));
301
            }
302 3
            $offset = $this->formats[$index]['Width'] *
303 3
                $this->formats[$index]['Height'] *
304 3
                $this->formats[$index]['BitCount'] / 8;
305 3
            $total_bytes = ($width * $this->formats[$index]['Height']) / 8;
306 3
            $maskBits = '';
307 3
            $bytes = 0;
308 3
            $bytes_per_line = ($this->formats[$index]['Width'] / 8);
309 3
            $bytes_to_remove = (($width - $this->formats[$index]['Width']) / 8);
310 3
            for ($i = 0; $i < $total_bytes; ++$i) {
311 3
                $maskBits .= str_pad(decbin(ord($this->formats[$index]['data'][$offset + $i])), 8, '0', STR_PAD_LEFT);
312 3
                ++$bytes;
313 3
                if ($bytes == $bytes_per_line) {
314 3
                    $i += $bytes_to_remove;
315 3
                    $bytes = 0;
316 3
                }
317 3
            }
318 3
        }
319
320
        // now paint pixels based on bit count
321 4
        switch ($this->formats[$index]['BitCount']) {
322 4
            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 3
            case 24:
345
                /**
346
                 * 24 bits: 3 bytes per pixel [ B | G | R ].
347
                 **/
348 1
                $offset = 0;
349 1
                $bitoffset = 0;
350 1
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
351 1
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
352 1
                        if ($maskBits[$bitoffset] == 0) {
353 1
                            $color = substr($this->formats[$index]['data'], $offset, 3);
354 1
                            $palette = $this->allocateColor($im, ord($color[2]), ord($color[1]), ord($color[0]));
355 1
                            imagesetpixel($im, $j, $i, $palette);
356 1
                        }
357 1
                        $offset += 3;
358 1
                        ++$bitoffset;
359 1
                    }
360 1
                }
361 1
                break;
362 2
            case 8:
363
                /**
364
                 * 8 bits: 1 byte per pixel [ COLOR INDEX ].
365
                 **/
366 1
                $offset = 0;
367 1
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
368 1
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
369 1
                        if ($maskBits[$offset] == 0) {
370 1
                            $color = ord(substr($this->formats[$index]['data'], $offset, 1));
371 1
                            imagesetpixel($im, $j, $i, $palette[$color]);
372 1
                        }
373 1
                        ++$offset;
374 1
                    }
375 1
                }
376 1
                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 4
        }
421
422 4
        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 4
    private function allocateColor(&$im, $red, $green, $blue, $alpha = 0)
442
    {
443 4
        $c = imagecolorexactalpha($im, $red, $green, $blue, $alpha);
444 4
        if ($c >= 0) {
445 4
            return $c;
446
        }
447
448
        return imagecolorallocatealpha($im, $red, $green, $blue, $alpha);
449
    }
450
}
451