Completed
Pull Request — master (#2)
by Paul
01:53
created

Ico   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 483
Duplicated Lines 6 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 98.23%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 65
c 4
b 1
f 0
lcom 1
cbo 0
dl 29
loc 483
ccs 278
cts 283
cp 0.9823
rs 5.7894

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 2
A loadFile() 0 15 3
C loadData() 0 42 7
A extract24BitData() 0 11 1
B extract8BitData() 0 28 2
B extract1BitData() 0 28 1
A extractIconDirEntries() 0 15 3
A getTotalIcons() 0 4 1
A getIconInfo() 0 8 2
A setBackground() 0 10 3
A setBackgroundTransparent() 0 4 1
C getImage() 0 36 8
B render32bit() 7 24 4
B render24bit() 5 23 4
A render8bit() 9 19 4
B render4bit() 0 27 5
B render1bit() 8 24 5
A buildPalette() 0 17 3
B buildMaskBits() 0 24 4
A allocateColor() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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
 * 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 $iconDirEntry;
24
25
    /**
26
     * Constructor
27
     *
28
     * @param string $path optional path to ICO file
29
     */
30 6
    public function __construct($path = '')
31
    {
32 6
        if (strlen($path) > 0) {
33 5
            $this->loadFile($path);
34 5
        }
35 6
    }
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 6
    public function loadFile($path)
46
    {
47 6
        $this->filename = $path;
48 6
        if (($fp = @fopen($path, 'rb')) !== false) {
49 6
            $data = '';
50 6
            while (!feof($fp)) {
51 6
                $data .= fread($fp, 4096);
52 6
            }
53 6
            fclose($fp);
54
55 6
            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 6
    private function loadData($data)
71
    {
72 6
        $this->iconDirEntry = [];
73
74
        //extract ICONDIR header
75 6
        $icodata = unpack('SReserved/SType/SCount', $data);
76 6
        $this->ico = $icodata;
77 6
        $data = substr($data, 6);
78
79
        //extract ICONDIRENTRY structures
80 6
        $data=$this->extractIconDirEntries($data);
81
82
        // Extract additional headers for each extracted icon header
83 6
        $iconCount = count($this->iconDirEntry);
84 6
        for ($i = 0; $i < $iconCount; ++$i) {
85 6
            $bitmapInfoHeader = unpack(
86
                'LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/' .
87 6
                'LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant',
88 6
                substr($data, $this->iconDirEntry[$i]['FileOffset'])
89 6
            );
90
91 6
            $this->iconDirEntry[$i]['header'] = $bitmapInfoHeader;
92 6
            $this->iconDirEntry[$i]['colors'] = [];
93 6
            $this->iconDirEntry[$i]['BitCount'] = $this->iconDirEntry[$i]['header']['BitCount'];
94
95 6
            switch ($this->iconDirEntry[$i]['BitCount']) {
96 6
                case 32:
97 6
                case 24:
98 3
                    $this->extract24BitData($i, $data);
99 3
                    break;
100 3
                case 8:
101 3
                case 4:
102 2
                    $this->extract8BitData($i, $data);
103 2
                    break;
104 1
                case 1:
105 1
                    $this->extract1BitData($i, $data);
106 1
                    break;
107 6
            }
108 6
            $this->iconDirEntry[$i]['data_length'] = strlen($this->iconDirEntry[$i]['data']);
109 6
        }
110 6
        return true;
111
    }
112
113 3
    private function extract24BitData($i, $data)
114
    {
115 3
        $length = $this->iconDirEntry[$i]['header']['Width'] *
116 3
            $this->iconDirEntry[$i]['header']['Height'] *
117 3
            ($this->iconDirEntry[$i]['BitCount'] / 8);
118 3
        $this->iconDirEntry[$i]['data'] = substr(
119 3
            $data,
120 3
            $this->iconDirEntry[$i]['FileOffset'] + $this->iconDirEntry[$i]['header']['Size'],
121
            $length
122 3
        );
123 3
    }
124
125 2
    private function extract8BitData($i, $data)
126
    {
127 2
        $icodata = substr(
128 2
            $data,
129 2
            $this->iconDirEntry[$i]['FileOffset'] + $this->iconDirEntry[$i]['header']['Size'],
130 2
            $this->iconDirEntry[$i]['ColorCount'] * 4
131 2
        );
132 2
        $offset = 0;
133 2
        for ($j = 0; $j < $this->iconDirEntry[$i]['ColorCount']; ++$j) {
134 2
            $this->iconDirEntry[$i]['colors'][] = [
135 2
                'blue' => ord($icodata[$offset]),
136 2
                'green' => ord($icodata[$offset + 1]),
137 2
                'red' => ord($icodata[$offset + 2]),
138 2
                'reserved' => ord($icodata[$offset + 3]),
139
            ];
140 2
            $offset += 4;
141 2
        }
142 2
        $length = $this->iconDirEntry[$i]['header']['Width'] *
143 2
            $this->iconDirEntry[$i]['header']['Height'] *
144 2
            (1 + $this->iconDirEntry[$i]['BitCount']) / $this->iconDirEntry[$i]['BitCount'];
145 2
        $this->iconDirEntry[$i]['data'] = substr(
146 2
            $data,
147 2
            $this->iconDirEntry[$i]['FileOffset'] +
148 2
            ($this->iconDirEntry[$i]['ColorCount'] * 4) +
149 2
            $this->iconDirEntry[$i]['header']['Size'],
150
            $length
151 2
        );
152 2
    }
153
154 1
    private function extract1BitData($i, $data)
155
    {
156 1
        $icodata = substr(
157 1
            $data,
158 1
            $this->iconDirEntry[$i]['FileOffset'] + $this->iconDirEntry[$i]['header']['Size'],
159 1
            $this->iconDirEntry[$i]['ColorCount'] * 4
160 1
        );
161
162 1
        $this->iconDirEntry[$i]['colors'][] = [
163 1
            'blue' => ord($icodata[0]),
164 1
            'green' => ord($icodata[1]),
165 1
            'red' => ord($icodata[2]),
166 1
            'reserved' => ord($icodata[3]),
167
        ];
168 1
        $this->iconDirEntry[$i]['colors'][] = [
169 1
            'blue' => ord($icodata[4]),
170 1
            'green' => ord($icodata[5]),
171 1
            'red' => ord($icodata[6]),
172 1
            'reserved' => ord($icodata[7]),
173
        ];
174
175 1
        $length = $this->iconDirEntry[$i]['header']['Width'] * $this->iconDirEntry[$i]['header']['Height'] / 8;
176 1
        $this->iconDirEntry[$i]['data'] = substr(
177 1
            $data,
178 1
            $this->iconDirEntry[$i]['FileOffset'] + $this->iconDirEntry[$i]['header']['Size'] + 8,
179
            $length
180 1
        );
181 1
    }
182
183 6
    private function extractIconDirEntries($data)
184
    {
185 6
        for ($i = 0; $i < $this->ico['Count']; ++$i) {
186 6
            $icodata = unpack('CWidth/CHeight/CColorCount/CReserved/SPlanes/SBitCount/LSizeInBytes/LFileOffset', $data);
187 6
            $icodata['FileOffset'] -= ($this->ico['Count'] * 16) + 6;
188 6
            if ($icodata['ColorCount'] == 0) {
189 4
                $icodata['ColorCount'] = 256;
190 4
            }
191 6
            $this->iconDirEntry[] = $icodata;
192
193 6
            $data = substr($data, 16);
194 6
        }
195
196 6
        return $data;
197
    }
198
    /**
199
     * Return the total icons extracted at the moment.
200
     *
201
     * @return int Total icons
202
     */
203 6
    public function getTotalIcons()
204
    {
205 6
        return count($this->iconDirEntry);
206
    }
207
208
    /**
209
     * Return the icon header corresponding to that index.
210
     *
211
     * @param int $index Icon index
212
     *
213
     * @return resource|bool Icon header or false
214
     */
215 6
    public function getIconInfo($index)
216
    {
217 6
        if (isset($this->iconDirEntry[$index])) {
218 6
            return $this->iconDirEntry[$index];
219
        }
220
221 1
        return false;
222
    }
223
224
    /**
225
     * Changes background color of extraction. You can set
226
     * the 3 color components or set $red = '#xxxxxx' (HTML format)
227
     * and leave all other blanks.
228
     *
229
     * @param int $red Red component
230
     * @param int $green Green component
231
     * @param int $blue Blue component
232
     */
233 5
    public function setBackground($red = 255, $green = 255, $blue = 255)
234
    {
235 5
        if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) {
236 5
            $green = hexdec($red[3] . $red[4]);
237 5
            $blue = hexdec($red[5] . $red[6]);
238 5
            $red = hexdec($red[1] . $red[2]);
239 5
        }
240
241 5
        $this->bgcolor = [$red, $green, $blue];
242 5
    }
243
244
    /**
245
     * Set background color to be saved as transparent.
246
     *
247
     * @param bool $transparent Is Transparent or not
248
     *
249
     * @return bool Is Transparent or not
250
     */
251 1
    public function setBackgroundTransparent($transparent = true)
252
    {
253 1
        return $this->bgcolorTransparent = $transparent;
254
    }
255
256
    /**
257
     * Return an image resource with the icon stored
258
     * on the $index position of the ICO file.
259
     *
260
     * @param int $index Position of the icon inside ICO
261
     *
262
     * @return resource|bool Image resource
263
     **/
264 6
    public function getImage($index)
265
    {
266 6
        if (!isset($this->iconDirEntry[$index])) {
267
            return false;
268
        }
269
270
        // create image filled with desired background color
271 6
        $im = imagecreatetruecolor($this->iconDirEntry[$index]['Width'], $this->iconDirEntry[$index]['Height']);
272 6
        $bgcolor = $this->allocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]);
273 6
        imagefilledrectangle($im, 0, 0, $this->iconDirEntry[$index]['Width'], $this->iconDirEntry[$index]['Height'], $bgcolor);
274
275 6
        if ($this->bgcolorTransparent) {
276 1
            imagecolortransparent($im, $bgcolor);
277 1
        }
278
279
        // now paint pixels based on bit count
280 6
        switch ($this->iconDirEntry[$index]['BitCount']) {
281 6
            case 32:
282 2
                $this->render32bit($this->iconDirEntry[$index], $im);
283 2
                break;
284 4
            case 24:
285 1
                $this->render24bit($this->iconDirEntry[$index], $im);
286 1
                break;
287 3
            case 8:
288 1
                $this->render8bit($this->iconDirEntry[$index], $im);
289 1
                break;
290 2
            case 4:
291 1
                $this->render4bit($this->iconDirEntry[$index], $im);
292 1
                break;
293 1
            case 1:
294 1
                $this->render1bit($this->iconDirEntry[$index], $im);
295 1
                break;
296 6
        }
297
298 6
        return $im;
299
    }
300
301 2
    private function render32bit($metadata, $im)
302
    {
303
        /**
304
         * 32 bits: 4 bytes per pixel [ B | G | R | ALPHA ].
305
         **/
306 2
        $offset = 0;
307 2
        $binary=$metadata['data'];
308
309 2
        for ($i = $metadata['Height'] - 1; $i >= 0; --$i) {
310 2
            for ($j = 0; $j < $metadata['Width']; ++$j) {
311
                //we translate the BGRA to aRGB ourselves, which is twice as fast
312
                //as calling imagecolorallocatealpha
313 2
                $alpha7 = ((~ord($binary[$offset+3])) & 0xff) >> 1;
314 2 View Code Duplication
                if ($alpha7 < 127) {
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...
315 2
                    $col = ($alpha7 << 24) |
316 2
                        (ord($binary[$offset+2]) << 16) |
317 2
                        (ord($binary[$offset+1]) << 8) |
318 2
                        (ord($binary[$offset]));
319 2
                    imagesetpixel($im, $j, $i, $col);
320 2
                }
321 2
                $offset += 4;
322 2
            }
323 2
        }
324 2
    }
325
326 1
    private function render24bit($metadata, $im)
327
    {
328 1
        $maskBits = $this->buildMaskBits($metadata);
329
330
        /**
331
         * 24 bits: 3 bytes per pixel [ B | G | R ].
332
         **/
333 1
        $offset = 0;
334 1
        $bitoffset = 0;
335 1
        $binary=$metadata['data'];
336
337 1
        for ($i = $metadata['Height'] - 1; $i >= 0; --$i) {
338 1
            for ($j = 0; $j < $metadata['Width']; ++$j) {
339 1 View Code Duplication
                if ($maskBits[$bitoffset] == 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...
340
                    //translate BGR to RGB
341 1
                    $col = (ord($binary[$offset+2]) << 16) | (ord($binary[$offset+1]) << 8) | (ord($binary[$offset]));
342 1
                    imagesetpixel($im, $j, $i, $col);
343 1
                }
344 1
                $offset += 3;
345 1
                ++$bitoffset;
346 1
            }
347 1
        }
348 1
    }
349
350 1
    private function render8bit($metadata, $im)
351
    {
352 1
        $palette = $this->buildPalette($metadata, $im);
353 1
        $maskBits = $this->buildMaskBits($metadata);
354
355
        /**
356
         * 8 bits: 1 byte per pixel [ COLOR INDEX ].
357
         **/
358 1
        $offset = 0;
359 1 View Code Duplication
        for ($i = $metadata['Height'] - 1; $i >= 0; --$i) {
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...
360 1
            for ($j = 0; $j < $metadata['Width']; ++$j) {
361 1
                if ($maskBits[$offset] == 0) {
362 1
                    $color = ord($metadata['data'][$offset]);
363 1
                    imagesetpixel($im, $j, $i, $palette[$color]);
364 1
                }
365 1
                ++$offset;
366 1
            }
367 1
        }
368 1
    }
369
370 1
    private function render4bit($metadata, $im)
371
    {
372 1
        $palette = $this->buildPalette($metadata, $im);
373 1
        $maskBits = $this->buildMaskBits($metadata);
374
375
        /**
376
         * 4 bits: half byte/nibble per pixel [ COLOR INDEX ].
377
         **/
378 1
        $offset = 0;
379 1
        $maskoffset = 0;
380 1
        for ($i = $metadata['Height'] - 1; $i >= 0; --$i) {
381 1
            for ($j = 0; $j < $metadata['Width']; $j += 2) {
382 1
                $colorByte = ord($metadata['data'][$offset]);
383 1
                $lowNibble = $colorByte & 0x0f;
384 1
                $highNibble = ($colorByte & 0xf0) >> 4;
385
386 1
                if ($maskBits[$maskoffset++] == 0) {
387 1
                    imagesetpixel($im, $j, $i, $palette[$highNibble]);
388 1
                }
389
390 1
                if ($maskBits[$maskoffset++] == 0) {
391 1
                    imagesetpixel($im, $j + 1, $i, $palette[$lowNibble]);
392 1
                }
393 1
                $offset++;
394 1
            }
395 1
        }
396 1
    }
397
398 1
    private function render1bit($metadata, $im)
399
    {
400 1
        $palette = $this->buildPalette($metadata, $im);
401 1
        $maskBits = $this->buildMaskBits($metadata);
402
403
        /**
404
         * 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ].
405
         **/
406 1
        $colorbits = '';
407 1
        $total = strlen($metadata['data']);
408 1
        for ($i = 0; $i < $total; ++$i) {
409 1
            $colorbits .= str_pad(decbin(ord($metadata['data'][$i])), 8, '0', STR_PAD_LEFT);
410 1
        }
411
412 1
        $offset = 0;
413 1 View Code Duplication
        for ($i = $metadata['Height'] - 1; $i >= 0; --$i) {
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...
414 1
            for ($j = 0; $j < $metadata['Width']; ++$j) {
415 1
                if ($maskBits[$offset] == 0) {
416 1
                    imagesetpixel($im, $j, $i, $palette[$colorbits[$offset]]);
417 1
                }
418 1
                ++$offset;
419 1
            }
420 1
        }
421 1
    }
422
423 3
    private function buildPalette($metadata, $im)
424
    {
425 3
        $palette = [];
426 3
        if ($metadata['BitCount'] != 24) {
427 3
            $palette = [];
428 3
            for ($i = 0; $i < $metadata['ColorCount']; ++$i) {
429 3
                $palette[$i] = $this->allocateColor(
430 3
                    $im,
431 3
                    $metadata['colors'][$i]['red'],
432 3
                    $metadata['colors'][$i]['green'],
433 3
                    $metadata['colors'][$i]['blue'],
434 3
                    round($metadata['colors'][$i]['reserved'] / 255 * 127)
435 3
                );
436 3
            }
437 3
        }
438 3
        return $palette;
439
    }
440
441 4
    private function buildMaskBits($metadata)
442
    {
443 4
        $width = $metadata['Width'];
444 4
        if (($width % 32) > 0) {
445
            $width += (32 - ($metadata['Width'] % 32));
446
        }
447 4
        $offset = $metadata['Width'] *
448 4
            $metadata['Height'] *
449 4
            $metadata['BitCount'] / 8;
450 4
        $total_bytes = ($width * $metadata['Height']) / 8;
451 4
        $maskBits = '';
452 4
        $bytes = 0;
453 4
        $bytes_per_line = ($metadata['Width'] / 8);
454 4
        $bytes_to_remove = (($width - $metadata['Width']) / 8);
455 4
        for ($i = 0; $i < $total_bytes; ++$i) {
456 4
            $maskBits .= str_pad(decbin(ord($metadata['data'][$offset + $i])), 8, '0', STR_PAD_LEFT);
457 4
            ++$bytes;
458 4
            if ($bytes == $bytes_per_line) {
459 4
                $i += $bytes_to_remove;
460 4
                $bytes = 0;
461 4
            }
462 4
        }
463 4
        return $maskBits;
464
    }
465
466
    /**
467
     * Allocate a color on $im resource. This function prevents
468
     * from allocating same colors on the same pallete. Instead
469
     * if it finds that the color is already allocated, it only
470
     * returns the index to that color.
471
     * It supports alpha channel.
472
     *
473
     * @param resource $im Image resource
474
     * @param int $red Red component
475
     * @param int $green Green component
476
     * @param int $blue Blue component
477
     * @param int $alpha Alpha channel
478
     *
479
     * @return int Color index
480
     */
481 6
    private function allocateColor(&$im, $red, $green, $blue, $alpha = 0)
482
    {
483 6
        $c = imagecolorexactalpha($im, $red, $green, $blue, $alpha);
484 6
        if ($c >= 0) {
485 6
            return $c;
486
        }
487
488
        return imagecolorallocatealpha($im, $red, $green, $blue, $alpha);
489
    }
490
}
491