Completed
Push — master ( 77e695...312aa1 )
by Paul
11s
created

Ico::extractPng()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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