Completed
Push — refactor ( b1bacf...888b68 )
by Paul
04:15 queued 01:47
created

Ico::getTotalIcons()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
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
            }
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
            }
100 1
            $this->formats[] = $icodata;
101
102 1
            $data = substr($data, 16);
103
        }
104
105
        /*
106
         * Extract aditional headers for each extracted icon header
107
         **/
108 1
        for ($i = 0; $i < count($this->formats); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
109 1
            $icodata = unpack(
110
                'LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/'.
111 1
                'LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant',
112 1
                substr($data, $this->formats[$i]['FileOffset'])
113
            );
114
115 1
            $this->formats[$i]['header'] = $icodata;
116 1
            $this->formats[$i]['colors'] = [];
117
118 1
            $this->formats[$i]['BitCount'] = $this->formats[$i]['header']['BitCount'];
119
120 1
            switch ($this->formats[$i]['BitCount']) {
121 1
                case 32:
122
                case 24:
123 1
                    $length = $this->formats[$i]['header']['Width'] *
124 1
                        $this->formats[$i]['header']['Height'] *
125 1
                        ($this->formats[$i]['BitCount'] / 8);
126 1
                    $this->formats[$i]['data'] = substr(
127
                        $data,
128 1
                        $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'],
129
                        $length
130
                    );
131 1
                    break;
132
                case 8:
133
                case 4:
134
                    $icodata = substr(
135
                        $data,
136
                        $this->formats[$i]['FileOffset'] + $icodata['Size'],
137
                        $this->formats[$i]['ColorCount'] * 4
138
                    );
139
                    $offset = 0;
140
                    for ($j = 0; $j < $this->formats[$i]['ColorCount']; ++$j) {
141
                        $this->formats[$i]['colors'][] = [
142
                            'red' => ord($icodata[$offset]),
143
                            'green' => ord($icodata[$offset + 1]),
144
                            'blue' => ord($icodata[$offset + 2]),
145
                            'reserved' => ord($icodata[$offset + 3]),
146
                        ];
147
                        $offset += 4;
148
                    }
149
                    $length = $this->formats[$i]['header']['Width'] *
150
                        $this->formats[$i]['header']['Height'] *
151
                        (1 + $this->formats[$i]['BitCount']) / $this->formats[$i]['BitCount'];
152
                    $this->formats[$i]['data'] = substr(
153
                        $data,
154
                        $this->formats[$i]['FileOffset'] +
155
                            ($this->formats[$i]['ColorCount'] * 4) +
156
                            $this->formats[$i]['header']['Size'],
157
                        $length
158
                    );
159
                    break;
160
                case 1:
161
                    $icodata = substr(
162
                        $data,
163
                        $this->formats[$i]['FileOffset'] + $icodata['Size'],
164
                        $this->formats[$i]['ColorCount'] * 4
165
                    );
166
167
                    $this->formats[$i]['colors'][] = [
168
                        'blue' => ord($icodata[0]),
169
                        'green' => ord($icodata[1]),
170
                        'red' => ord($icodata[2]),
171
                        'reserved' => ord($icodata[3]),
172
                    ];
173
                    $this->formats[$i]['colors'][] = [
174
                        'blue' => ord($icodata[4]),
175
                        'green' => ord($icodata[5]),
176
                        'red' => ord($icodata[6]),
177
                        'reserved' => ord($icodata[7]),
178
                    ];
179
180
                    $length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] / 8;
181
                    $this->formats[$i]['data'] = substr(
182
                        $data,
183
                        $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'] + 8,
184
                        $length
185
                    );
186
                    break;
187
            }
188 1
            $this->formats[$i]['data_length'] = strlen($this->formats[$i]['data']);
189
        }
190
191 1
        return true;
192
    }
193
194
    /**
195
     * Return the total icons extracted at the moment.
196
     *
197
     * @return int Total icons
198
     **/
199 1
    public function getTotalIcons()
200
    {
201 1
        return count($this->formats);
202
    }
203
204
    /**
205
     * Ico::GetIconInfo()
206
     * Return the icon header corresponding to that index.
207
     *
208
     * @param int $index Icon index
209
     *
210
     * @return resource|bool Icon header or false
211
     **/
212 1
    public function getIconInfo($index)
213
    {
214 1
        if (isset($this->formats[$index])) {
215 1
            return $this->formats[$index];
216
        }
217
218 1
        return false;
219
    }
220
221
    /**
222
     * Changes background color of extraction. You can set
223
     * the 3 color components or set $red = '#xxxxxx' (HTML format)
224
     * and leave all other blanks.
225
     *
226
     * @param int $red   Red component
227
     * @param int $green Green component
228
     * @param int $blue  Blue component
229
     **/
230 1
    public function setBackground($red = 255, $green = 255, $blue = 255)
231
    {
232 1
        if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) {
233 1
            $green = hexdec($red[3].$red[4]);
234 1
            $blue = hexdec($red[5].$red[6]);
235 1
            $red = hexdec($red[1].$red[2]);
236
        }
237
238 1
        $this->bgcolor = [$red, $green, $blue];
239 1
    }
240
241
    /**
242
     * Set background color to be saved as transparent.
243
     *
244
     * @param bool $is_transparent Is Transparent or not
245
     *
246
     * @return bool Is Transparent or not
247
     **/
248
    public function setBackgroundTransparent($is_transparent = true)
249
    {
250
        return $this->bgcolorTransparent = $is_transparent;
251
    }
252
253
    /**
254
     * Return an image resource with the icon stored
255
     * on the $index position of the ICO file.
256
     *
257
     * @param int $index Position of the icon inside ICO
258
     *
259
     * @return resource|bool Image resource
260
     **/
261 1
    public function getImage($index)
262
    {
263 1
        if (!isset($this->formats[$index])) {
264
            return false;
265
        }
266
267
        /**
268
         * create image.
269
         **/
270 1
        $im = imagecreatetruecolor($this->formats[$index]['Width'], $this->formats[$index]['Height']);
271
272
        /**
273
         * paint background.
274
         **/
275 1
        $bgcolor = $this->allocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]);
276 1
        imagefilledrectangle($im, 0, 0, $this->formats[$index]['Width'], $this->formats[$index]['Height'], $bgcolor);
277
278
        /*
279
         * set background color transparent
280
         **/
281 1
        if ($this->bgcolorTransparent) {
282
            imagecolortransparent($im, $bgcolor);
283
        }
284
285
        /*
286
         * allocate pallete and get XOR image
287
         **/
288 1
        if (in_array($this->formats[$index]['BitCount'], [1, 4, 8, 24])) {
289
            if ($this->formats[$index]['BitCount'] != 24) {
290
                /**
291
                 * color pallete.
292
                 **/
293
                $c = [];
294
                for ($i = 0; $i < $this->formats[$index]['ColorCount']; ++$i) {
295
                    $c[$i] = $this->allocateColor(
296
                        $im,
297
                        $this->formats[$index]['colors'][$i]['red'],
298
                        $this->formats[$index]['colors'][$i]['green'],
299
                        $this->formats[$index]['colors'][$i]['blue'],
300
                        round($this->formats[$index]['colors'][$i]['reserved'] / 255 * 127)
301
                    );
302
                }
303
            }
304
305
            /**
306
             * XOR image.
307
             **/
308
            $width = $this->formats[$index]['Width'];
309
            if (($width % 32) > 0) {
310
                $width += (32 - ($this->formats[$index]['Width'] % 32));
311
            }
312
            $offset = $this->formats[$index]['Width'] *
313
                $this->formats[$index]['Height'] *
314
                $this->formats[$index]['BitCount'] / 8;
315
            $total_bytes = ($width * $this->formats[$index]['Height']) / 8;
316
            $bits = '';
317
            $bytes = 0;
318
            $bytes_per_line = ($this->formats[$index]['Width'] / 8);
319
            $bytes_to_remove = (($width - $this->formats[$index]['Width']) / 8);
320
            for ($i = 0; $i < $total_bytes; ++$i) {
321
                $bits .= str_pad(decbin(ord($this->formats[$index]['data'][$offset + $i])), 8, '0', STR_PAD_LEFT);
322
                ++$bytes;
323
                if ($bytes == $bytes_per_line) {
324
                    $i += $bytes_to_remove;
325
                    $bytes = 0;
326
                }
327
            }
328
        }
329
330
        /*
331
         * paint each pixel depending on bit count
332
         **/
333 1
        switch ($this->formats[$index]['BitCount']) {
334 1
            case 32:
335
                /**
336
                 * 32 bits: 4 bytes per pixel [ B | G | R | ALPHA ].
337
                 **/
338 1
                $offset = 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
                        $color = substr($this->formats[$index]['data'], $offset, 4);
342 1
                        if (ord($color[3]) > 0) {
343 1
                            $c = $this->allocateColor(
344
                                $im,
345 1
                                ord($color[2]),
346 1
                                ord($color[1]),
347 1
                                ord($color[0]),
348 1
                                127 - round(ord($color[3]) / 255 * 127)
349
                            );
350 1
                            imagesetpixel($im, $j, $i, $c);
351
                        }
352 1
                        $offset += 4;
353
                    }
354
                }
355 1
                break;
356
            case 24:
357
                /**
358
                 * 24 bits: 3 bytes per pixel [ B | G | R ].
359
                 **/
360
                $offset = 0;
361
                $bitoffset = 0;
362
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
363
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
364
                        if ($bits[$bitoffset] == 0) {
0 ignored issues
show
Bug introduced by
The variable $bits does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
365
                            $color = substr($this->formats[$index]['data'], $offset, 3);
366
                            $c = $this->allocateColor($im, ord($color[2]), ord($color[1]), ord($color[0]));
367
                            imagesetpixel($im, $j, $i, $c);
368
                        }
369
                        $offset += 3;
370
                        ++$bitoffset;
371
                    }
372
                }
373
                break;
374
            case 8:
375
                /**
376
                 * 8 bits: 1 byte per pixel [ COLOR INDEX ].
377
                 **/
378
                $offset = 0;
379
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
380
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
381
                        if ($bits[$offset] == 0) {
382
                            $color = ord(substr($this->formats[$index]['data'], $offset, 1));
383
                            imagesetpixel($im, $j, $i, $c[$color]);
0 ignored issues
show
Bug introduced by
The variable $c does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
384
                        }
385
                        ++$offset;
386
                    }
387
                }
388
                break;
389
            case 4:
390
                /**
391
                 * 4 bits: half byte/nibble per pixel [ COLOR INDEX ].
392
                 **/
393
                $offset = 0;
394
                $maskoffset = 0;
395
                $leftbits = true;
396
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
397
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
398
                        if ($leftbits) {
399
                            $color = substr($this->formats[$index]['data'], $offset, 1);
400
                            $color = [
401
                                'High' => bindec(substr(decbin(ord($color)), 0, 4)),
402
                                'Low' => bindec(substr(decbin(ord($color)), 4)),
403
                            ];
404 View Code Duplication
                            if ($bits[$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...
405
                                imagesetpixel($im, $j, $i, $c[$color['High']]);
406
                            }
407
                            $leftbits = false;
408 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...
409
                            if ($bits[$maskoffset++] == 0) {
410
                                imagesetpixel($im, $j, $i, $c[$color['Low']]);
0 ignored issues
show
Bug introduced by
The variable $color does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
411
                            }
412
                            ++$offset;
413
                            $leftbits = true;
414
                        }
415
                    }
416
                }
417
                break;
418
            case 1:
419
                /**
420
                 * 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ].
421
                 **/
422
                $colorbits = '';
423
                $total = strlen($this->formats[$index]['data']);
424
                for ($i = 0; $i < $total; ++$i) {
425
                    $colorbits .= str_pad(decbin(ord($this->formats[$index]['data'][$i])), 8, '0', STR_PAD_LEFT);
426
                }
427
428
                //$total = strlen($colorbits);
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
429
                $offset = 0;
430
                for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) {
431
                    for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) {
432
                        if ($bits[$offset] == 0) {
433
                            imagesetpixel($im, $j, $i, $c[$colorbits[$offset]]);
434
                        }
435
                        ++$offset;
436
                    }
437
                }
438
                break;
439
        }
440
441 1
        return $im;
442
    }
443
444
    /**
445
     * Ico::AllocateColor()
446
     * Allocate a color on $im resource. This function prevents
447
     * from allocating same colors on the same pallete. Instead
448
     * if it finds that the color is already allocated, it only
449
     * returns the index to that color.
450
     * It supports alpha channel.
451
     *
452
     * @param resource $im     Image resource
453
     * @param int      $red    Red component
454
     * @param int      $green  Green component
455
     * @param int      $blue   Blue component
456
     * @param int      $alphpa Alpha channel
0 ignored issues
show
Documentation introduced by
There is no parameter named $alphpa. Did you maybe mean $alpha?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
457
     *
458
     * @return int Color index
459
     **/
460 1
    private function allocateColor(&$im, $red, $green, $blue, $alpha = 0)
461
    {
462 1
        $c = imagecolorexactalpha($im, $red, $green, $blue, $alpha);
463 1
        if ($c >= 0) {
464 1
            return $c;
465
        }
466
467
        return imagecolorallocatealpha($im, $red, $green, $blue, $alpha);
468
    }
469
}
470