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

Ico::extract1BitData()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 28
ccs 22
cts 22
cp 1
rs 8.8571
cc 1
eloc 20
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 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