Completed
Push — master ( 99fd1f...77e695 )
by Paul
8s
created

Ico::getImage()   C

Complexity

Conditions 8
Paths 13

Size

Total Lines 43
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 8.0017

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 43
ccs 32
cts 33
cp 0.9697
rs 5.3846
cc 8
eloc 31
nc 13
nop 1
crap 8.0017
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
    /**
200
     * Return the total icons extracted at the moment.
201
     *
202
     * @return int Total icons
203
     */
204 6
    public function getTotalIcons()
205
    {
206 6
        return count($this->iconDirEntry);
207
    }
208
209
    /**
210
     * Return the icon header corresponding to that index.
211
     *
212
     * @param int $index Icon index
213
     *
214
     * @return resource|bool Icon header or false
215
     */
216 6
    public function getIconInfo($index)
217
    {
218 6
        if (isset($this->iconDirEntry[$index])) {
219 6
            return $this->iconDirEntry[$index];
220
        }
221
222 1
        return false;
223
    }
224
225
    /**
226
     * Changes background color of extraction. You can set
227
     * the 3 color components or set $red = '#xxxxxx' (HTML format)
228
     * and leave all other blanks.
229
     *
230
     * @param int $red Red component
231
     * @param int $green Green component
232
     * @param int $blue Blue component
233
     */
234 5
    public function setBackground($red = 255, $green = 255, $blue = 255)
235
    {
236 5
        if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) {
237 5
            $green = hexdec($red[3] . $red[4]);
238 5
            $blue = hexdec($red[5] . $red[6]);
239 5
            $red = hexdec($red[1] . $red[2]);
240 5
        }
241
242 5
        $this->bgcolor = [$red, $green, $blue];
243 5
    }
244
245
    /**
246
     * Set background color to be saved as transparent.
247
     *
248
     * @param bool $transparent Is Transparent or not
249
     *
250
     * @return bool Is Transparent or not
251
     */
252 1
    public function setBackgroundTransparent($transparent = true)
253
    {
254 1
        return $this->bgcolorTransparent = $transparent;
255
    }
256
257
    /**
258
     * Return an image resource with the icon stored
259
     * on the $index position of the ICO file.
260
     *
261
     * @param int $index Position of the icon inside ICO
262
     *
263
     * @return resource|bool Image resource
264
     **/
265 6
    public function getImage($index)
266
    {
267 6
        if (!isset($this->iconDirEntry[$index])) {
268
            return false;
269
        }
270
271
        // create image filled with desired background color
272 6
        $im = imagecreatetruecolor($this->iconDirEntry[$index]['Width'], $this->iconDirEntry[$index]['Height']);
273 6
        $bgcolor = $this->allocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]);
274 6
        imagefilledrectangle(
275 6
            $im,
276 6
            0,
277 6
            0,
278 6
            $this->iconDirEntry[$index]['Width'],
279 6
            $this->iconDirEntry[$index]['Height'],
280
            $bgcolor
281 6
        );
282
283 6
        if ($this->bgcolorTransparent) {
284 1
            imagecolortransparent($im, $bgcolor);
285 1
        }
286
287
        // now paint pixels based on bit count
288 6
        switch ($this->iconDirEntry[$index]['BitCount']) {
289 6
            case 32:
290 2
                $this->render32bit($this->iconDirEntry[$index], $im);
291 2
                break;
292 4
            case 24:
293 1
                $this->render24bit($this->iconDirEntry[$index], $im);
294 1
                break;
295 3
            case 8:
296 1
                $this->render8bit($this->iconDirEntry[$index], $im);
297 1
                break;
298 2
            case 4:
299 1
                $this->render4bit($this->iconDirEntry[$index], $im);
300 1
                break;
301 1
            case 1:
302 1
                $this->render1bit($this->iconDirEntry[$index], $im);
303 1
                break;
304 6
        }
305
306 6
        return $im;
307
    }
308
309 2
    private function render32bit($metadata, $im)
310
    {
311
        /**
312
         * 32 bits: 4 bytes per pixel [ B | G | R | ALPHA ].
313
         **/
314 2
        $offset = 0;
315 2
        $binary = $metadata['data'];
316
317 2
        for ($i = $metadata['Height'] - 1; $i >= 0; --$i) {
318 2
            for ($j = 0; $j < $metadata['Width']; ++$j) {
319
                //we translate the BGRA to aRGB ourselves, which is twice as fast
320
                //as calling imagecolorallocatealpha
321 2
                $alpha7 = ((~ord($binary[$offset + 3])) & 0xff) >> 1;
322 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...
323 2
                    $col = ($alpha7 << 24) |
324 2
                        (ord($binary[$offset + 2]) << 16) |
325 2
                        (ord($binary[$offset + 1]) << 8) |
326 2
                        (ord($binary[$offset]));
327 2
                    imagesetpixel($im, $j, $i, $col);
328 2
                }
329 2
                $offset += 4;
330 2
            }
331 2
        }
332 2
    }
333
334 1
    private function render24bit($metadata, $im)
335
    {
336 1
        $maskBits = $this->buildMaskBits($metadata);
337
338
        /**
339
         * 24 bits: 3 bytes per pixel [ B | G | R ].
340
         **/
341 1
        $offset = 0;
342 1
        $bitoffset = 0;
343 1
        $binary = $metadata['data'];
344
345 1
        for ($i = $metadata['Height'] - 1; $i >= 0; --$i) {
346 1
            for ($j = 0; $j < $metadata['Width']; ++$j) {
347 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...
348
                    //translate BGR to RGB
349 1
                    $col = (ord($binary[$offset + 2]) << 16) |
350 1
                        (ord($binary[$offset + 1]) << 8) |
351 1
                        (ord($binary[$offset]));
352 1
                    imagesetpixel($im, $j, $i, $col);
353 1
                }
354 1
                $offset += 3;
355 1
                ++$bitoffset;
356 1
            }
357 1
        }
358 1
    }
359
360 1
    private function render8bit($metadata, $im)
361
    {
362 1
        $palette = $this->buildPalette($metadata, $im);
363 1
        $maskBits = $this->buildMaskBits($metadata);
364
365
        /**
366
         * 8 bits: 1 byte per pixel [ COLOR INDEX ].
367
         **/
368 1
        $offset = 0;
369 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...
370 1
            for ($j = 0; $j < $metadata['Width']; ++$j) {
371 1
                if ($maskBits[$offset] == 0) {
372 1
                    $color = ord($metadata['data'][$offset]);
373 1
                    imagesetpixel($im, $j, $i, $palette[$color]);
374 1
                }
375 1
                ++$offset;
376 1
            }
377 1
        }
378 1
    }
379
380 1
    private function render4bit($metadata, $im)
381
    {
382 1
        $palette = $this->buildPalette($metadata, $im);
383 1
        $maskBits = $this->buildMaskBits($metadata);
384
385
        /**
386
         * 4 bits: half byte/nibble per pixel [ COLOR INDEX ].
387
         **/
388 1
        $offset = 0;
389 1
        $maskoffset = 0;
390 1
        for ($i = $metadata['Height'] - 1; $i >= 0; --$i) {
391 1
            for ($j = 0; $j < $metadata['Width']; $j += 2) {
392 1
                $colorByte = ord($metadata['data'][$offset]);
393 1
                $lowNibble = $colorByte & 0x0f;
394 1
                $highNibble = ($colorByte & 0xf0) >> 4;
395
396 1
                if ($maskBits[$maskoffset++] == 0) {
397 1
                    imagesetpixel($im, $j, $i, $palette[$highNibble]);
398 1
                }
399
400 1
                if ($maskBits[$maskoffset++] == 0) {
401 1
                    imagesetpixel($im, $j + 1, $i, $palette[$lowNibble]);
402 1
                }
403 1
                $offset++;
404 1
            }
405 1
        }
406 1
    }
407
408 1
    private function render1bit($metadata, $im)
409
    {
410 1
        $palette = $this->buildPalette($metadata, $im);
411 1
        $maskBits = $this->buildMaskBits($metadata);
412
413
        /**
414
         * 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ].
415
         **/
416 1
        $colorbits = '';
417 1
        $total = strlen($metadata['data']);
418 1
        for ($i = 0; $i < $total; ++$i) {
419 1
            $colorbits .= str_pad(decbin(ord($metadata['data'][$i])), 8, '0', STR_PAD_LEFT);
420 1
        }
421
422 1
        $offset = 0;
423 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...
424 1
            for ($j = 0; $j < $metadata['Width']; ++$j) {
425 1
                if ($maskBits[$offset] == 0) {
426 1
                    imagesetpixel($im, $j, $i, $palette[$colorbits[$offset]]);
427 1
                }
428 1
                ++$offset;
429 1
            }
430 1
        }
431 1
    }
432
433 3
    private function buildPalette($metadata, $im)
434
    {
435 3
        $palette = [];
436 3
        if ($metadata['BitCount'] != 24) {
437 3
            $palette = [];
438 3
            for ($i = 0; $i < $metadata['ColorCount']; ++$i) {
439 3
                $palette[$i] = $this->allocateColor(
440 3
                    $im,
441 3
                    $metadata['colors'][$i]['red'],
442 3
                    $metadata['colors'][$i]['green'],
443 3
                    $metadata['colors'][$i]['blue'],
444 3
                    round($metadata['colors'][$i]['reserved'] / 255 * 127)
445 3
                );
446 3
            }
447 3
        }
448 3
        return $palette;
449
    }
450
451 4
    private function buildMaskBits($metadata)
452
    {
453 4
        $width = $metadata['Width'];
454 4
        if (($width % 32) > 0) {
455
            $width += (32 - ($metadata['Width'] % 32));
456
        }
457 4
        $offset = $metadata['Width'] *
458 4
            $metadata['Height'] *
459 4
            $metadata['BitCount'] / 8;
460 4
        $total_bytes = ($width * $metadata['Height']) / 8;
461 4
        $maskBits = '';
462 4
        $bytes = 0;
463 4
        $bytes_per_line = ($metadata['Width'] / 8);
464 4
        $bytes_to_remove = (($width - $metadata['Width']) / 8);
465 4
        for ($i = 0; $i < $total_bytes; ++$i) {
466 4
            $maskBits .= str_pad(decbin(ord($metadata['data'][$offset + $i])), 8, '0', STR_PAD_LEFT);
467 4
            ++$bytes;
468 4
            if ($bytes == $bytes_per_line) {
469 4
                $i += $bytes_to_remove;
470 4
                $bytes = 0;
471 4
            }
472 4
        }
473 4
        return $maskBits;
474
    }
475
476
    /**
477
     * Allocate a color on $im resource. This function prevents
478
     * from allocating same colors on the same pallete. Instead
479
     * if it finds that the color is already allocated, it only
480
     * returns the index to that color.
481
     * It supports alpha channel.
482
     *
483
     * @param resource $im Image resource
484
     * @param int $red Red component
485
     * @param int $green Green component
486
     * @param int $blue Blue component
487
     * @param int $alpha Alpha channel
488
     *
489
     * @return int Color index
490
     */
491 6
    private function allocateColor(&$im, $red, $green, $blue, $alpha = 0)
492
    {
493 6
        $c = imagecolorexactalpha($im, $red, $green, $blue, $alpha);
494 6
        if ($c >= 0) {
495 6
            return $c;
496
        }
497
498
        return imagecolorallocatealpha($im, $red, $green, $blue, $alpha);
499
    }
500
}
501