Completed
Push — master ( 3ad606...cfe2d1 )
by Paul
02:19
created

IcoParser::isPNG()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Elphin\IcoFileLoader;
4
5
/**
6
 * IcoParser provides the means to read an ico file and produce an Icon object
7
 * containing an IconImage objects for each image in the ico file
8
 *
9
 * @package Elphin\IcoFileLoader
10
 */
11
class IcoParser implements ParserInterface
12
{
13
    /**
14
     * @inheritdoc
15
     */
16
    public function isSupportedBinaryString($data)
17
    {
18
        $supported = !is_null($this->parseIconDir($data));
19
        $supported = $supported || $this->isPNG($data);
20
        return $supported;
21
    }
22
23
    /**
24
     * Reads the ICONDIR header and verifies it looks sane
25
     * @param string $data
26
     * @return array|null - null is returned if the file doesn't look like an .ico file
27
     */
28
    private function parseIconDir($data)
29
    {
30
        $icondir = unpack('SReserved/SType/SCount', $data);
31
        if ($icondir['Reserved'] == 0 && $icondir['Type'] == 1) {
32
            return $icondir;
33
        }
34
        return null;
35
    }
36
37
    /**
38
     * @param $data
39
     * @return bool true if first four bytes look like a PNG
40
     */
41
    private function isPNG($data)
42
    {
43
        $signature = unpack('LFourCC', $data);
44
        return ($signature['FourCC'] == 0x474e5089);
45
    }
46
47
    /**
48
     * @inheritdoc
49
     */
50
    public function parse($data)
51
    {
52
        if ($this->isPNG($data)) {
53
            return $this->parsePNGAsIco($data);
54
        }
55
        return $this->parseICO($data);
56
    }
57
58
    private function parseICO($data)
59
    {
60
        $icondir = $this->parseIconDir($data);
61
        if (!$icondir) {
62
            throw new \InvalidArgumentException('Invalid ICO file format');
63
        }
64
65
        //nibble the header off our data
66
        $data = substr($data, 6);
67
68
        //parse the ICONDIRENTRY headers
69
        $icon = new Icon();
70
        $data = $this->parseIconDirEntries($icon, $data, $icondir['Count']);
71
72
        // Extract additional headers for each extracted ICONDIRENTRY
73
        $iconCount = count($icon);
74
        for ($i = 0; $i < $iconCount; ++$i) {
75
            if ($this->isPNG(substr($data, $icon[$i]->fileOffset, 4))) {
76
                $this->parsePng($icon[$i], $data);
77
            } else {
78
                $this->parseBmp($icon[$i], $data);
79
            }
80
        }
81
82
        return $icon;
83
    }
84
85
    private function parsePNGAsIco($data)
86
    {
87
        $png = imagecreatefromstring($data);
88
        $w = imagesx($png);
89
        $h = imagesy($png);
90
        $bits = imageistruecolor($png) ? 32 : 8;
91
        imagedestroy($png);
92
93
        //fake enough header data for IconImage to do its job
94
        $icoDirEntry = [
95
            'width' => $w,
96
            'height' => $h,
97
            'bitCount' => $bits
98
        ];
99
100
        //create the iconimage and give it the PNG data
101
        $image = new IconImage($icoDirEntry);
102
        $image->setPngFile($data);
103
104
        $icon = new Icon();
105
        $icon[] = $image;
106
        return $icon;
107
    }
108
109
    /**
110
     * Parse the sequence of ICONDIRENTRY structures
111
     * @param Icon $icon
112
     * @param string $data
113
     * @param integer $count
114
     * @return string
115
     */
116
    private function parseIconDirEntries(Icon $icon, $data, $count)
117
    {
118
        for ($i = 0; $i < $count; ++$i) {
119
            $icoDirEntry = unpack(
120
                'Cwidth/Cheight/CcolorCount/Creserved/Splanes/SbitCount/LsizeInBytes/LfileOffset',
121
                $data
122
            );
123
            $icoDirEntry['fileOffset'] -= ($count * 16) + 6;
124
            if ($icoDirEntry['colorCount'] == 0) {
125
                $icoDirEntry['colorCount'] = 256;
126
            }
127
            if ($icoDirEntry['width'] == 0) {
128
                $icoDirEntry['width'] = 256;
129
            }
130
            if ($icoDirEntry['height'] == 0) {
131
                $icoDirEntry['height'] = 256;
132
            }
133
134
            $entry = new IconImage($icoDirEntry);
135
            $icon[] = $entry;
136
137
            $data = substr($data, 16);
138
        }
139
140
        return $data;
141
    }
142
143
    /**
144
     * Handle icon image which is PNG formatted
145
     * @param IconImage $entry
146
     * @param string $data
147
     */
148
    private function parsePng(IconImage $entry, $data)
149
    {
150
        //a png icon contains a complete png image at the file offset
151
        $png = substr($data, $entry->fileOffset, $entry->sizeInBytes);
152
        $entry->setPngFile($png);
153
    }
154
155
    /**
156
     * Handle icon image which is BMP formatted
157
     * @param IconImage $entry
158
     * @param string $data
159
     */
160
    private function parseBmp(IconImage $entry, $data)
161
    {
162
        $bitmapInfoHeader = unpack(
163
            'LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/' .
164
            'LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant',
165
            substr($data, $entry->fileOffset, 40)
166
        );
167
168
        $entry->setBitmapInfoHeader($bitmapInfoHeader);
169
170
        switch ($entry->bitCount) {
171
            case 32:
172
            case 24:
173
                $this->parseTrueColorImageData($entry, $data);
174
                break;
175
            case 8:
176
            case 4:
177
            case 1:
178
                $this->parsePaletteImageData($entry, $data);
179
                break;
180
        }
181
    }
182
183
    /**
184
     * Parse an image which doesn't use a palette
185
     * @param IconImage $entry
186
     * @param string $data
187
     */
188
    private function parseTrueColorImageData(IconImage $entry, $data)
189
    {
190
        $length = $entry->bmpHeaderWidth * $entry->bmpHeaderHeight * ($entry->bitCount / 8);
191
        $bmpData = substr($data, $entry->fileOffset + $entry->bmpHeaderSize, $length);
192
        $entry->setBitmapData($bmpData);
193
    }
194
195
    /**
196
     * Parse an image which uses a limited palette of colours
197
     * @param IconImage $entry
198
     * @param string $data
199
     */
200
    private function parsePaletteImageData(IconImage $entry, $data)
201
    {
202
        $pal = substr($data, $entry->fileOffset + $entry->bmpHeaderSize, $entry->colorCount * 4);
203
        $idx = 0;
204
        for ($j = 0; $j < $entry->colorCount; ++$j) {
205
            $entry->addToBmpPalette(ord($pal[$idx + 2]), ord($pal[$idx + 1]), ord($pal[$idx]), ord($pal[$idx + 3]));
206
            $idx += 4;
207
        }
208
209
        $length = $entry->bmpHeaderWidth * $entry->bmpHeaderHeight * (1 + $entry->bitCount) / $entry->bitCount;
210
        $bmpData = substr($data, $entry->fileOffset + $entry->bmpHeaderSize + $entry->colorCount * 4, $length);
211
        $entry->setBitmapData($bmpData);
212
    }
213
}
214