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
|
1 |
|
} |
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
|
1 |
|
} |
100
|
1 |
|
$this->formats[] = $icodata; |
101
|
|
|
|
102
|
1 |
|
$data = substr($data, 16); |
103
|
1 |
|
} |
104
|
|
|
|
105
|
|
|
/* |
106
|
|
|
* Extract aditional headers for each extracted icon header |
107
|
|
|
**/ |
108
|
1 |
|
$formatCount=count($this->formats); |
109
|
1 |
|
for ($i = 0; $i < $formatCount; ++$i) { |
110
|
1 |
|
$icodata = unpack( |
111
|
|
|
'LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/'. |
112
|
1 |
|
'LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant', |
113
|
1 |
|
substr($data, $this->formats[$i]['FileOffset']) |
114
|
1 |
|
); |
115
|
|
|
|
116
|
1 |
|
$this->formats[$i]['header'] = $icodata; |
117
|
1 |
|
$this->formats[$i]['colors'] = []; |
118
|
|
|
|
119
|
1 |
|
$this->formats[$i]['BitCount'] = $this->formats[$i]['header']['BitCount']; |
120
|
|
|
|
121
|
1 |
|
switch ($this->formats[$i]['BitCount']) { |
122
|
1 |
|
case 32: |
123
|
1 |
|
case 24: |
124
|
1 |
|
$length = $this->formats[$i]['header']['Width'] * |
125
|
1 |
|
$this->formats[$i]['header']['Height'] * |
126
|
1 |
|
($this->formats[$i]['BitCount'] / 8); |
127
|
1 |
|
$this->formats[$i]['data'] = substr( |
128
|
1 |
|
$data, |
129
|
1 |
|
$this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'], |
130
|
|
|
$length |
131
|
1 |
|
); |
132
|
1 |
|
break; |
133
|
|
|
case 8: |
134
|
|
|
case 4: |
135
|
|
|
$icodata = substr( |
136
|
|
|
$data, |
137
|
|
|
$this->formats[$i]['FileOffset'] + $icodata['Size'], |
138
|
|
|
$this->formats[$i]['ColorCount'] * 4 |
139
|
|
|
); |
140
|
|
|
$offset = 0; |
141
|
|
|
for ($j = 0; $j < $this->formats[$i]['ColorCount']; ++$j) { |
142
|
|
|
$this->formats[$i]['colors'][] = [ |
143
|
|
|
'red' => ord($icodata[$offset]), |
144
|
|
|
'green' => ord($icodata[$offset + 1]), |
145
|
|
|
'blue' => ord($icodata[$offset + 2]), |
146
|
|
|
'reserved' => ord($icodata[$offset + 3]), |
147
|
|
|
]; |
148
|
|
|
$offset += 4; |
149
|
|
|
} |
150
|
|
|
$length = $this->formats[$i]['header']['Width'] * |
151
|
|
|
$this->formats[$i]['header']['Height'] * |
152
|
|
|
(1 + $this->formats[$i]['BitCount']) / $this->formats[$i]['BitCount']; |
153
|
|
|
$this->formats[$i]['data'] = substr( |
154
|
|
|
$data, |
155
|
|
|
$this->formats[$i]['FileOffset'] + |
156
|
|
|
($this->formats[$i]['ColorCount'] * 4) + |
157
|
|
|
$this->formats[$i]['header']['Size'], |
158
|
|
|
$length |
159
|
|
|
); |
160
|
|
|
break; |
161
|
|
|
case 1: |
162
|
|
|
$icodata = substr( |
163
|
|
|
$data, |
164
|
|
|
$this->formats[$i]['FileOffset'] + $icodata['Size'], |
165
|
|
|
$this->formats[$i]['ColorCount'] * 4 |
166
|
|
|
); |
167
|
|
|
|
168
|
|
|
$this->formats[$i]['colors'][] = [ |
169
|
|
|
'blue' => ord($icodata[0]), |
170
|
|
|
'green' => ord($icodata[1]), |
171
|
|
|
'red' => ord($icodata[2]), |
172
|
|
|
'reserved' => ord($icodata[3]), |
173
|
|
|
]; |
174
|
|
|
$this->formats[$i]['colors'][] = [ |
175
|
|
|
'blue' => ord($icodata[4]), |
176
|
|
|
'green' => ord($icodata[5]), |
177
|
|
|
'red' => ord($icodata[6]), |
178
|
|
|
'reserved' => ord($icodata[7]), |
179
|
|
|
]; |
180
|
|
|
|
181
|
|
|
$length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] / 8; |
182
|
|
|
$this->formats[$i]['data'] = substr( |
183
|
|
|
$data, |
184
|
|
|
$this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'] + 8, |
185
|
|
|
$length |
186
|
|
|
); |
187
|
|
|
break; |
188
|
1 |
|
} |
189
|
1 |
|
$this->formats[$i]['data_length'] = strlen($this->formats[$i]['data']); |
190
|
1 |
|
} |
191
|
|
|
|
192
|
1 |
|
return true; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Return the total icons extracted at the moment. |
197
|
|
|
* |
198
|
|
|
* @return int Total icons |
199
|
|
|
**/ |
200
|
1 |
|
public function getTotalIcons() |
201
|
|
|
{ |
202
|
1 |
|
return count($this->formats); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Ico::GetIconInfo() |
207
|
|
|
* Return the icon header corresponding to that index. |
208
|
|
|
* |
209
|
|
|
* @param int $index Icon index |
210
|
|
|
* |
211
|
|
|
* @return resource|bool Icon header or false |
212
|
|
|
**/ |
213
|
1 |
|
public function getIconInfo($index) |
214
|
|
|
{ |
215
|
1 |
|
if (isset($this->formats[$index])) { |
216
|
1 |
|
return $this->formats[$index]; |
217
|
|
|
} |
218
|
|
|
|
219
|
1 |
|
return false; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Changes background color of extraction. You can set |
224
|
|
|
* the 3 color components or set $red = '#xxxxxx' (HTML format) |
225
|
|
|
* and leave all other blanks. |
226
|
|
|
* |
227
|
|
|
* @param int $red Red component |
228
|
|
|
* @param int $green Green component |
229
|
|
|
* @param int $blue Blue component |
230
|
|
|
**/ |
231
|
1 |
|
public function setBackground($red = 255, $green = 255, $blue = 255) |
232
|
|
|
{ |
233
|
1 |
|
if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) { |
234
|
1 |
|
$green = hexdec($red[3].$red[4]); |
235
|
1 |
|
$blue = hexdec($red[5].$red[6]); |
236
|
1 |
|
$red = hexdec($red[1].$red[2]); |
237
|
1 |
|
} |
238
|
|
|
|
239
|
1 |
|
$this->bgcolor = [$red, $green, $blue]; |
240
|
1 |
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Set background color to be saved as transparent. |
244
|
|
|
* |
245
|
|
|
* @param bool $is_transparent Is Transparent or not |
246
|
|
|
* |
247
|
|
|
* @return bool Is Transparent or not |
248
|
|
|
**/ |
249
|
|
|
public function setBackgroundTransparent($is_transparent = true) |
250
|
|
|
{ |
251
|
|
|
return $this->bgcolorTransparent = $is_transparent; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* Return an image resource with the icon stored |
256
|
|
|
* on the $index position of the ICO file. |
257
|
|
|
* |
258
|
|
|
* @param int $index Position of the icon inside ICO |
259
|
|
|
* |
260
|
|
|
* @return resource|bool Image resource |
261
|
|
|
**/ |
262
|
1 |
|
public function getImage($index) |
263
|
|
|
{ |
264
|
1 |
|
if (!isset($this->formats[$index])) { |
265
|
|
|
return false; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
// create image filled with desired background color |
269
|
1 |
|
$im = imagecreatetruecolor($this->formats[$index]['Width'], $this->formats[$index]['Height']); |
270
|
1 |
|
$bgcolor = $this->allocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]); |
271
|
1 |
|
imagefilledrectangle($im, 0, 0, $this->formats[$index]['Width'], $this->formats[$index]['Height'], $bgcolor); |
272
|
|
|
|
273
|
1 |
|
if ($this->bgcolorTransparent) { |
274
|
|
|
imagecolortransparent($im, $bgcolor); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
//we may build a string of 1/0 to represent the XOR mask |
278
|
1 |
|
$maskBits=''; |
279
|
|
|
//we may build a palette for 8 bit images |
280
|
1 |
|
$palette=[]; |
281
|
|
|
|
282
|
|
|
// allocate palette and get XOR image |
283
|
1 |
|
if (in_array($this->formats[$index]['BitCount'], [1, 4, 8, 24])) { |
284
|
|
|
if ($this->formats[$index]['BitCount'] != 24) { |
285
|
|
|
$palette = []; |
286
|
|
|
for ($i = 0; $i < $this->formats[$index]['ColorCount']; ++$i) { |
287
|
|
|
$palette[$i] = $this->allocateColor( |
288
|
|
|
$im, |
289
|
|
|
$this->formats[$index]['colors'][$i]['red'], |
290
|
|
|
$this->formats[$index]['colors'][$i]['green'], |
291
|
|
|
$this->formats[$index]['colors'][$i]['blue'], |
292
|
|
|
round($this->formats[$index]['colors'][$i]['reserved'] / 255 * 127) |
293
|
|
|
); |
294
|
|
|
} |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
// build XOR mask bits |
298
|
|
|
$width = $this->formats[$index]['Width']; |
299
|
|
|
if (($width % 32) > 0) { |
300
|
|
|
$width += (32 - ($this->formats[$index]['Width'] % 32)); |
301
|
|
|
} |
302
|
|
|
$offset = $this->formats[$index]['Width'] * |
303
|
|
|
$this->formats[$index]['Height'] * |
304
|
|
|
$this->formats[$index]['BitCount'] / 8; |
305
|
|
|
$total_bytes = ($width * $this->formats[$index]['Height']) / 8; |
306
|
|
|
$maskBits = ''; |
307
|
|
|
$bytes = 0; |
308
|
|
|
$bytes_per_line = ($this->formats[$index]['Width'] / 8); |
309
|
|
|
$bytes_to_remove = (($width - $this->formats[$index]['Width']) / 8); |
310
|
|
|
for ($i = 0; $i < $total_bytes; ++$i) { |
311
|
|
|
$maskBits .= str_pad(decbin(ord($this->formats[$index]['data'][$offset + $i])), 8, '0', STR_PAD_LEFT); |
312
|
|
|
++$bytes; |
313
|
|
|
if ($bytes == $bytes_per_line) { |
314
|
|
|
$i += $bytes_to_remove; |
315
|
|
|
$bytes = 0; |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
// now paint pixels based on bit count |
321
|
1 |
|
switch ($this->formats[$index]['BitCount']) { |
322
|
1 |
|
case 32: |
323
|
|
|
/** |
324
|
|
|
* 32 bits: 4 bytes per pixel [ B | G | R | ALPHA ]. |
325
|
|
|
**/ |
326
|
1 |
|
$offset = 0; |
327
|
1 |
|
for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
328
|
1 |
|
for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) { |
329
|
1 |
|
$color = substr($this->formats[$index]['data'], $offset, 4); |
330
|
1 |
|
if (ord($color[3]) > 0) { |
331
|
1 |
|
$palette = $this->allocateColor( |
332
|
1 |
|
$im, |
333
|
1 |
|
ord($color[2]), |
334
|
1 |
|
ord($color[1]), |
335
|
1 |
|
ord($color[0]), |
336
|
1 |
|
127 - round(ord($color[3]) / 255 * 127) |
337
|
1 |
|
); |
338
|
1 |
|
imagesetpixel($im, $j, $i, $palette); |
339
|
1 |
|
} |
340
|
1 |
|
$offset += 4; |
341
|
1 |
|
} |
342
|
1 |
|
} |
343
|
1 |
|
break; |
344
|
|
|
case 24: |
345
|
|
|
/** |
346
|
|
|
* 24 bits: 3 bytes per pixel [ B | G | R ]. |
347
|
|
|
**/ |
348
|
|
|
$offset = 0; |
349
|
|
|
$bitoffset = 0; |
350
|
|
|
for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
351
|
|
|
for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) { |
352
|
|
|
if ($maskBits[$bitoffset] == 0) { |
353
|
|
|
$color = substr($this->formats[$index]['data'], $offset, 3); |
354
|
|
|
$palette = $this->allocateColor($im, ord($color[2]), ord($color[1]), ord($color[0])); |
355
|
|
|
imagesetpixel($im, $j, $i, $palette); |
356
|
|
|
} |
357
|
|
|
$offset += 3; |
358
|
|
|
++$bitoffset; |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
break; |
362
|
|
|
case 8: |
363
|
|
|
/** |
364
|
|
|
* 8 bits: 1 byte per pixel [ COLOR INDEX ]. |
365
|
|
|
**/ |
366
|
|
|
$offset = 0; |
367
|
|
|
for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
368
|
|
|
for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) { |
369
|
|
|
if ($maskBits[$offset] == 0) { |
370
|
|
|
$color = ord(substr($this->formats[$index]['data'], $offset, 1)); |
371
|
|
|
imagesetpixel($im, $j, $i, $palette[$color]); |
372
|
|
|
} |
373
|
|
|
++$offset; |
374
|
|
|
} |
375
|
|
|
} |
376
|
|
|
break; |
377
|
|
|
case 4: |
378
|
|
|
/** |
379
|
|
|
* 4 bits: half byte/nibble per pixel [ COLOR INDEX ]. |
380
|
|
|
**/ |
381
|
|
|
$offset = 0; |
382
|
|
|
$maskoffset = 0; |
383
|
|
|
$leftbits = true; |
384
|
|
|
for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
385
|
|
|
for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) { |
386
|
|
|
$colorByte = substr($this->formats[$index]['data'], $offset, 1); |
387
|
|
|
$color = [ |
388
|
|
|
'High' => bindec(substr(decbin(ord($colorByte)), 0, 4)), |
389
|
|
|
'Low' => bindec(substr(decbin(ord($colorByte)), 4)), |
390
|
|
|
]; |
391
|
|
|
|
392
|
|
|
if ($leftbits) { |
393
|
|
View Code Duplication |
if ($maskBits[$maskoffset++] == 0) { |
|
|
|
|
394
|
|
|
imagesetpixel($im, $j, $i, $palette[$color['High']]); |
395
|
|
|
} |
396
|
|
|
$leftbits = false; |
397
|
|
View Code Duplication |
} else { |
|
|
|
|
398
|
|
|
if ($maskBits[$maskoffset++] == 0) { |
399
|
|
|
imagesetpixel($im, $j, $i, $palette[$color['Low']]); |
400
|
|
|
} |
401
|
|
|
++$offset; |
402
|
|
|
$leftbits = true; |
403
|
|
|
} |
404
|
|
|
} |
405
|
|
|
} |
406
|
|
|
break; |
407
|
|
|
case 1: |
408
|
|
|
/** |
409
|
|
|
* 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ]. |
410
|
|
|
**/ |
411
|
|
|
$colorbits = ''; |
412
|
|
|
$total = strlen($this->formats[$index]['data']); |
413
|
|
|
for ($i = 0; $i < $total; ++$i) { |
414
|
|
|
$colorbits .= str_pad(decbin(ord($this->formats[$index]['data'][$i])), 8, '0', STR_PAD_LEFT); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
$offset = 0; |
418
|
|
|
for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; --$i) { |
419
|
|
|
for ($j = 0; $j < $this->formats[$index]['Width']; ++$j) { |
420
|
|
|
if ($maskBits[$offset] == 0) { |
421
|
|
|
imagesetpixel($im, $j, $i, $palette[$colorbits[$offset]]); |
422
|
|
|
} |
423
|
|
|
++$offset; |
424
|
|
|
} |
425
|
|
|
} |
426
|
|
|
break; |
427
|
1 |
|
} |
428
|
|
|
|
429
|
1 |
|
return $im; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* Ico::AllocateColor() |
434
|
|
|
* Allocate a color on $im resource. This function prevents |
435
|
|
|
* from allocating same colors on the same pallete. Instead |
436
|
|
|
* if it finds that the color is already allocated, it only |
437
|
|
|
* returns the index to that color. |
438
|
|
|
* It supports alpha channel. |
439
|
|
|
* |
440
|
|
|
* @param resource $im Image resource |
441
|
|
|
* @param int $red Red component |
442
|
|
|
* @param int $green Green component |
443
|
|
|
* @param int $blue Blue component |
444
|
|
|
* @param int $alpha Alpha channel |
445
|
|
|
* |
446
|
|
|
* @return int Color index |
447
|
|
|
**/ |
448
|
1 |
|
private function allocateColor(&$im, $red, $green, $blue, $alpha = 0) |
449
|
|
|
{ |
450
|
1 |
|
$c = imagecolorexactalpha($im, $red, $green, $blue, $alpha); |
451
|
1 |
|
if ($c >= 0) { |
452
|
1 |
|
return $c; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
return imagecolorallocatealpha($im, $red, $green, $blue, $alpha); |
456
|
|
|
} |
457
|
|
|
} |
458
|
|
|
|
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.