1<?php
2/**
3 * Class Ico
4 * Open ICO files and extract any size/depth to PNG format
5 * http://www.phpclasses.org/package/2369-PHP-Extract-graphics-from-ico-files-into-PNG-images.html
6 * @author Diogo Resende <me@diogoresende.net>
7 * @version 0.1
8 * @license GPL
9 **/
10class ico {
11	/**
12	 * Ico::bgcolor
13	 * Background color on icon extraction
14	 *
15	 * @type array(R, G, B) = array(255, 255, 255)
16	 * @var  public
17	 **/
18	var $bgcolor = array(255, 255, 255);
19
20	/**
21	 * Ico::bgcolor_transparent
22	 * Is background color transparent?
23	 *
24	 * @type boolean = false
25	 * @var  public
26	 **/
27	var $bgcolor_transparent = false;
28
29	/**
30	 * Class constructor
31	 *
32	 * @param   optional    string   $path   Path to ICO file
33	 * @return              void
34	 **/
35	function __construct($path = '') {
36		if (strlen($path) > 0) {
37			$this->LoadFile($path);
38		}
39	}
40
41	/**
42	 * Ico::Ico()
43	 *
44	 * @param   optional    string   $path   Path to ICO file
45	 * @return              void
46	 **/
47	function Ico($path = '') {
48		self::__construct($path);
49	}
50
51	/**
52	 * Ico::LoadFile()
53	 * Load an ICO file (don't need to call this is if fill the
54	 * parameter in the class constructor)
55	 *
56	 * @param   string   $path   Path to ICO file
57	 * @return  boolean          Success
58	 **/
59	function LoadFile($path) {
60		$this->_filename = $path;
61		if (($fp = @fopen($path, 'rb')) !== false) {
62			$data = '';
63			while (!feof($fp)) {
64				$data .= fread($fp, 4096);
65			}
66			fclose($fp);
67
68			return $this->LoadData($data);
69		}
70		return false;
71	}
72
73	/**
74	 * Ico::LoadData()
75	 * Load an ICO data. If you prefer to open the file
76	 * and return the binary data you can use this function
77	 * directly. Otherwise use LoadFile() instead.
78	 *
79	 * @param   string   $data   Binary data of ICO file
80	 * @return  boolean          Success
81	 **/
82	function LoadData($data) {
83		$this->formats = array();
84
85		/**
86		 * ICO header
87		 **/
88		$icodata = unpack("vReserved/vType/vCount", $data);
89		$this->ico = $icodata;
90		$data = substr($data, 6);
91
92		// Accept ICO files only.
93		if (($this->ico['Reserved'] != 0) || ($this->ico['Type'] != 1)) {
94			return False;
95		}
96
97		/**
98		 * Extract each icon header
99		 **/
100		for ($i = 0; $i < $this->ico['Count']; $i ++) {
101			if (strlen($data))
102			{
103				$icodata = unpack("CWidth/CHeight/CColorCount/CReserved/SPlanes/SBitCount/LSizeInBytes/LFileOffset", $data);
104				$icodata['FileOffset'] -= ($this->ico['Count'] * 16) + 6;
105				if ($icodata['ColorCount'] == 0) $icodata['ColorCount'] = 256;
106				$this->formats[] = $icodata;
107
108				$data = substr($data, 16);
109			}
110			else
111			{
112				error_log(__METHOD__.__LINE__.' no data available:'.array2string($this->ico));
113				break;
114			}
115		}
116
117		/**
118		 * Extract aditional headers for each extracted icon header
119		 **/
120		for ($i = 0; $i < count($this->formats); $i++) {
121			$icodata = unpack("LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant", substr($data, $this->formats[$i]['FileOffset']));
122
123			$this->formats[$i]['header'] = $icodata;
124			$this->formats[$i]['colors'] = array();
125
126			$this->formats[$i]['BitCount'] = $this->formats[$i]['header']['BitCount'];
127
128			switch ($this->formats[$i]['BitCount']) {
129				case 32:
130				case 24:
131					$length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] * ($this->formats[$i]['BitCount'] / 8);
132					$this->formats[$i]['data'] = substr($data, $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'], $length);
133					break;
134				case 8:
135				case 4:
136					$icodata = substr($data, $this->formats[$i]['FileOffset'] + $icodata['Size'], $this->formats[$i]['ColorCount'] * 4);
137					$offset = 0;
138					for ($j = 0; $j < $this->formats[$i]['ColorCount']; $j++) {
139						$this->formats[$i]['colors'][] = array(
140							'red'     => ord($icodata[$offset]),
141							'green'    => ord($icodata[$offset + 1]),
142							'blue'      => ord($icodata[$offset + 2]),
143							'reserved' => ord($icodata[$offset + 3])
144						);
145						$offset += 4;
146					}
147					$length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] * (1 + $this->formats[$i]['BitCount']) / $this->formats[$i]['BitCount'];
148					$this->formats[$i]['data'] = substr($data, $this->formats[$i]['FileOffset'] + ($this->formats[$i]['ColorCount'] * 4) + $this->formats[$i]['header']['Size'], $length);
149					break;
150				case 1:
151					$icodata = substr($data, $this->formats[$i]['FileOffset'] + $icodata['Size'], $this->formats[$i]['ColorCount'] * 4);
152
153					$this->formats[$i]['colors'][] = array(
154							'blue'     => ord($icodata[0]),
155							'green'    => ord($icodata[1]),
156							'red'      => ord($icodata[2]),
157							'reserved' => ord($icodata[3])
158					);
159					$this->formats[$i]['colors'][] = array(
160							'blue'     => ord($icodata[4]),
161							'green'    => ord($icodata[5]),
162							'red'      => ord($icodata[6]),
163							'reserved' => ord($icodata[7])
164					);
165
166					$length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] / 8;
167					$this->formats[$i]['data'] = substr($data, $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'] + 8, $length);
168					break;
169			}
170			$this->formats[$i]['data_length'] = strlen($this->formats[$i]['data']);
171		}
172
173		return true;
174	}
175
176	/**
177	 * Ico::TotalIcons()
178	 * Return the total icons extracted at the moment
179	 *
180	 * @return  integer   Total icons
181	 **/
182	function TotalIcons() {
183		return count($this->formats);
184	}
185
186	/**
187	 * Ico::GetIconInfo()
188	 * Return the icon header corresponding to that index
189	 *
190	 * @param   integer   $index    Icon index
191	 * @return  resource            Icon header
192	 **/
193	function GetIconInfo($index) {
194		if (isset($this->formats[$index])) {
195			return $this->formats[$index];
196		}
197		return false;
198	}
199
200	/**
201	 * Ico::SetBackground()
202	 * Changes background color of extraction. You can set
203	 * the 3 color components or set $red = '#xxxxxx' (HTML format)
204	 * and leave all other blanks.
205	 *
206	 * @param   optional   integer   $red     Red component
207	 * @param   optional   integer   $green   Green component
208	 * @param   optional   integer   $blue    Blue component
209	 * @return             void
210	 **/
211	function SetBackground($red = 255, $green = 255, $blue = 255) {
212		if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) {
213			$green = hexdec($red[3] . $red[4]);
214			$blue = hexdec($red[5] . $red[6]);
215			$red = hexdec($red[1] . $red[2]);
216		}
217
218		$this->bgcolor = array($red, $green, $blue);
219	}
220
221	/**
222	 * Ico::SetBackgroundTransparent()
223	 * Set background color to be saved as transparent
224	 *
225	 * @param   optional   boolean   $is_transparent   Is Transparent or not
226	 * @return             boolean                     Is Transparent or not
227	 **/
228	function SetBackgroundTransparent($is_transparent = true) {
229		return ($this->bgcolor_transparent = $is_transparent);
230	}
231
232	/**
233	 * Ico::GetImage()
234	 * Return an image resource with the icon stored
235	 * on the $index position of the ICO file
236	 *
237	 * @param   integer    $index   Position of the icon inside ICO
238	 * @return  resource            Image resource
239	 **/
240	function &GetIcon($index) {
241		if (is_null($index)) {
242			$index = count($this->formats)-1;
243			$index = 0;
244		}
245		if (!isset($this->formats[$index])) {
246			return false;
247		}
248
249		/**
250		 * create image
251		 **/
252		$im = imagecreatetruecolor($this->formats[$index]['Width'], $this->formats[$index]['Height']);
253
254		/**
255		 * paint background
256		 **/
257		$bgcolor = $this->AllocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]);
258		imagefilledrectangle($im, 0 , 0, $this->formats[$index]['Width'], $this->formats[$index]['Height'], $bgcolor);
259
260		/**
261		 * set background color transparent
262		 **/
263		if ($this->bgcolor_transparent) {
264			imagecolortransparent($im, $bgcolor);
265		}
266
267		/**
268		 * allocate pallete and get XOR image
269		 **/
270		if (in_array($this->formats[$index]['BitCount'], array(1, 4, 8, 24))) {
271			if ($this->formats[$index]['BitCount'] != 24) {
272				/**
273				 * color pallete
274				 **/
275				$c = array();
276				for ($i = 0; $i < $this->formats[$index]['ColorCount']; $i++) {
277					$c[$i] = $this->AllocateColor($im, $this->formats[$index]['colors'][$i]['blue'],
278						 $this->formats[$index]['colors'][$i]['green'],
279						 $this->formats[$index]['colors'][$i]['red'],
280						 round($this->formats[$index]['colors'][$i]['reserved'] / 255 * 127));
281				}
282			}
283
284			/**
285			 * XOR image
286			 **/
287			$width = $this->formats[$index]['Width'];
288			if (($width % 32) > 0) {
289				 $width += (32 - ($this->formats[$index]['Width'] % 32));
290			}
291			$offset = $this->formats[$index]['Width'] * $this->formats[$index]['Height'] * $this->formats[$index]['BitCount'] / 8;
292			$total_bytes = ($width * $this->formats[$index]['Height']) / 8;
293			$bits = '';
294			$bytes = 0;
295			$bytes_per_line = ($this->formats[$index]['Width'] / 8);
296			$bytes_to_remove = (($width - $this->formats[$index]['Width']) / 8);
297			for ($i = 0; $i < $total_bytes; $i++) {
298				$bits .= str_pad(decbin(ord($this->formats[$index]['data'][$offset + $i])), 8, '0', STR_PAD_LEFT);
299				$bytes++;
300				if ($bytes == $bytes_per_line) {
301					$i += $bytes_to_remove;
302					$bytes = 0;
303				}
304			}
305		}
306
307		/**
308		 * paint each pixel depending on bit count
309		 **/
310		switch ($this->formats[$index]['BitCount']) {
311			case 32:
312				/**
313				 * 32 bits: 4 bytes per pixel [ B | G | R | ALPHA ]
314				 **/
315				$offset = 0;
316				for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
317					for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
318						$color = substr($this->formats[$index]['data'], $offset, 4);
319						if (ord($color[3]) > 0) {
320							$c = $this->AllocateColor($im, ord($color[2]),
321														   ord($color[1]),
322														   ord($color[0]),
323														   127 - round(ord($color[3]) / 255 * 127));
324							imagesetpixel($im, $j, $i, $c);
325						}
326						$offset += 4;
327					}
328				}
329				break;
330			case 24:
331				/**
332				 * 24 bits: 3 bytes per pixel [ B | G | R ]
333				 **/
334				$offset = 0;
335				$bitoffset = 0;
336				for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
337					for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
338						if ($bits[$bitoffset] == 0) {
339							$color = substr($this->formats[$index]['data'], $offset, 3);
340							$c = $this->AllocateColor($im, ord($color[2]), ord($color[1]), ord($color[0]));
341							imagesetpixel($im, $j, $i, $c);
342						}
343						$offset += 3;
344						$bitoffset++;
345					}
346				}
347				break;
348			case 8:
349				/**
350				 * 8 bits: 1 byte per pixel [ COLOR INDEX ]
351				 **/
352				$offset = 0;
353				for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
354					for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
355						if ($bits[$offset] == 0) {
356							$color = ord(substr($this->formats[$index]['data'], $offset, 1));
357							imagesetpixel($im, $j, $i, $c[$color]);
358						}
359						$offset++;
360					}
361				}
362				break;
363			case 4:
364				/**
365				 * 4 bits: half byte/nibble per pixel [ COLOR INDEX ]
366				 **/
367				$offset = 0;
368				$maskoffset = 0;
369				$leftbits = true;
370				for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
371					for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
372						if ($leftbits) {
373							$color = substr($this->formats[$index]['data'], $offset, 1);
374							$color = array(
375								'High' => bindec(substr(decbin(ord($color)), 0, 4)),
376								'Low' => bindec(substr(decbin(ord($color)), 4))
377							);
378							if ($bits[$maskoffset++] == 0) {
379								imagesetpixel($im, $j, $i, $c[$color['High']]);
380							}
381							$leftbits = false;
382						} else {
383							if ($bits[$maskoffset++] == 0) {
384								imagesetpixel($im, $j, $i, $c[$color['Low']]);
385							}
386							$offset++;
387							$leftbits = true;
388						}
389					}
390				}
391				break;
392			case 1:
393				/**
394				 * 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ]
395				 **/
396				$colorbits = '';
397				$total = strlen($this->formats[$index]['data']);
398				for ($i = 0; $i < $total; $i++) {
399					$colorbits .= str_pad(decbin(ord($this->formats[$index]['data'][$i])), 8, '0', STR_PAD_LEFT);
400				}
401
402				$total = strlen($colorbits);
403				$offset = 0;
404				for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
405					for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
406						if ($bits[$offset] == 0) {
407							imagesetpixel($im, $j, $i, $c[$colorbits[$offset]]);
408						}
409						$offset++;
410					}
411				}
412				break;
413		}
414
415		return $im;
416	}
417
418	/**
419	 * Ico::AllocateColor()
420	 * Allocate a color on $im resource. This function prevents
421	 * from allocating same colors on the same pallete. Instead
422	 * if it finds that the color is already allocated, it only
423	 * returns the index to that color.
424	 * It supports alpha channel.
425	 *
426	 * @param               resource    $im       Image resource
427	 * @param               integer     $red      Red component
428	 * @param               integer     $green    Green component
429	 * @param               integer     $blue     Blue component
430	 * @param   optional    integer     $alphpa   Alpha channel
431	 * @return              integer               Color index
432	 **/
433	function AllocateColor(&$im, $red, $green, $blue, $alpha = 0) {
434		$c = imagecolorexactalpha($im, $red, $green, $blue, $alpha);
435		if ($c >= 0) {
436			return $c;
437		}
438		return imagecolorallocatealpha($im, $red, $green, $blue, $alpha);
439	}
440}
441