<?php
    /**
     * Class Ico
     * Open ICO files and extract any size/depth to PNG format
     *
     * @author Diogo Resende <me@diogoresende.net>
     * @version 0.1
     *
     * @method public  Ico($path = '')
     * @method public  LoadFile($path)
     * @method private LoadData($data)
     * @method public  TotalIcons()
     * @method public  GetIconInfo($index)
     * @method public  GetIcon($index)
     * @method private AllocateColor(&$im, $red, $green, $blue, $alpha = 0)
	 *
	 * 示例
	 * $ico = new Ico("http://www.baidu.com/favicon.ico");
	 * imagepng($ico->GetIcon(0), "favicon.png");
     **/
    class Ico {
        /**
         * Ico::bgcolor
         * Background color on icon extraction
         *
         * @type array(R, G, B) = array(255, 255, 255)
         * @var  public
         **/
        var $bgcolor = array(255, 255, 255);
        /**
         * Ico::bgcolor_transparent
         * Is background color transparent?
         *
         * @type boolean = false
         * @var  public
         **/
        var $bgcolor_transparent = false;
        /**
         * Ico::Ico()
         * Class constructor
         *
         * @param   optional    string   $path   Path to ICO file
         * @return              void
         **/
        function Ico($path = '') {
            if (strlen($path) > 0) {
                $this->LoadFile($path);
            }
        }
        /**
         * Ico::LoadFile()
         * Load an ICO file (don't need to call this is if fill the
         * parameter in the class constructor)
         *
         * @param   string   $path   Path to ICO file
         * @return  boolean          Success
         **/
        function LoadFile($path) {
         $this->_filename = $path;
            if (($fp = @fopen($path, 'rb')) !== false) {
                $data = '';
                while (!feof($fp)) {
                    $data .= fread($fp, 4096);
                }
                fclose($fp);
                return $this->LoadData($data);
            }
            return false;
        }
        /**
         * Ico::LoadData()
         * Load an ICO data. If you prefer to open the file
         * and return the binary data you can use this function
         * directly. Otherwise use LoadFile() instead.
         *
         * @param   string   $data   Binary data of ICO file
         * @return  boolean          Success
         **/
        function LoadData($data) {
            $this->formats = array();
            /**
             * ICO header
             **/
            $icodata = unpack("SReserved/SType/SCount", $data);
            $this->ico = $icodata;
            $data = substr($data, 6);
           
            /**
             * Extract each icon header
             **/
            for ($i = 0; $i < $this->ico['Count']; $i ++) {
                $icodata = unpack("CWidth/CHeight/CColorCount/CReserved/SPlanes/SBitCount/LSizeInBytes/LFileOffset", $data);
                $icodata['FileOffset'] -= ($this->ico['Count'] * 16) + 6;
                if ($icodata['ColorCount'] == 0) $icodata['ColorCount'] = 256;
                $this->formats[] = $icodata;
                $data = substr($data, 16);
            }
            /**
             * Extract aditional headers for each extracted icon header
             **/
            for ($i = 0; $i < count($this->formats); $i++) {
                $icodata = unpack("LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant", substr($data, $this->formats[$i]['FileOffset']));
                $this->formats[$i]['header'] = $icodata;
                $this->formats[$i]['colors'] = array();
                $this->formats[$i]['BitCount'] = $this->formats[$i]['header']['BitCount'];
               
                switch ($this->formats[$i]['BitCount']) {
                    case 32:
                    case 24:
                        $length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] * ($this->formats[$i]['BitCount'] / 8);
                        $this->formats[$i]['data'] = substr($data, $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'], $length);
                        break;
                    case 8:
                    case 4:
                        $icodata = substr($data, $this->formats[$i]['FileOffset'] + $icodata['Size'], $this->formats[$i]['ColorCount'] * 4);
                        $offset = 0;
                        for ($j = 0; $j < $this->formats[$i]['ColorCount']; $j++) {
                            $this->formats[$i]['colors'][] = array(
                                'red'     => ord($icodata[$offset]),
                                'green'    => ord($icodata[$offset + 1]),
                                'blue'      => ord($icodata[$offset + 2]),
                                'reserved' => ord($icodata[$offset + 3])
                            );
                            $offset += 4;
                        }
                        $length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] * (1 + $this->formats[$i]['BitCount']) / $this->formats[$i]['BitCount'];
                        $this->formats[$i]['data'] = substr($data, $this->formats[$i]['FileOffset'] + ($this->formats[$i]['ColorCount'] * 4) + $this->formats[$i]['header']['Size'], $length);
                        break;
                    case 1:
                        $icodata = substr($data, $this->formats[$i]['FileOffset'] + $icodata['Size'], $this->formats[$i]['ColorCount'] * 4);
                        $this->formats[$i]['colors'][] = array(
                                'blue'     => ord($icodata[0]),
                                'green'    => ord($icodata[1]),
                                'red'      => ord($icodata[2]),
                                'reserved' => ord($icodata[3])
                        );
                        $this->formats[$i]['colors'][] = array(
                                'blue'     => ord($icodata[4]),
                                'green'    => ord($icodata[5]),
                                'red'      => ord($icodata[6]),
                                'reserved' => ord($icodata[7])
                        );
                        $length = $this->formats[$i]['header']['Width'] * $this->formats[$i]['header']['Height'] / 8;
                        $this->formats[$i]['data'] = substr($data, $this->formats[$i]['FileOffset'] + $this->formats[$i]['header']['Size'] + 8, $length);
                        break;
                }
                $this->formats[$i]['data_length'] = strlen($this->formats[$i]['data']);
            }
           
            return true;
        }
        /**
         * Ico::TotalIcons()
         * Return the total icons extracted at the moment
         *
         * @return  integer   Total icons
         **/
        function TotalIcons() {
            return count($this->formats);
        }
       
        /**
         * Ico::GetIconInfo()
         * Return the icon header corresponding to that index
         *
         * @param   integer   $index    Icon index
         * @return  resource            Icon header
         **/
        function GetIconInfo($index) {
            if (isset($this->formats[$index])) {
                return $this->formats[$index];
            }
            return false;
        }
       
        /**
         * Ico::SetBackground()
         * Changes background color of extraction. You can set
         * the 3 color components or set $red = '#xxxxxx' (HTML format)
         * and leave all other blanks.
         *
         * @param   optional   integer   $red     Red component
         * @param   optional   integer   $green   Green component
         * @param   optional   integer   $blue    Blue component
         * @return             void
         **/
        function SetBackground($red = 255, $green = 255, $blue = 255) {
            if (is_string($red) && preg_match('/^\#[0-9a-f]{6}$/', $red)) {
                $green = hexdec($red[3] . $red[4]);
                $blue = hexdec($red[5] . $red[6]);
                $red = hexdec($red[1] . $red[2]);
            }
            $this->bgcolor = array($red, $green, $blue);
        }
       
        /**
         * Ico::SetBackgroundTransparent()
         * Set background color to be saved as transparent
         *
         * @param   optional   boolean   $is_transparent   Is Transparent or not
         * @return             boolean                     Is Transparent or not
         **/
        function SetBackgroundTransparent($is_transparent = true) {
            return ($this->bgcolor_transparent = $is_transparent);
        }
        /**
         * Ico::GetImage()
         * Return an image resource with the icon stored
         * on the $index position of the ICO file
         *
         * @param   integer    $index   Position of the icon inside ICO
         * @return  resource            Image resource
         **/
        function &GetIcon($index) {
            if (!isset($this->formats[$index])) {
                return false;
            }
           
            /**
             * create image
             **/
            $im = imagecreatetruecolor($this->formats[$index]['Width'], $this->formats[$index]['Height']);
            /**
             * paint background
             **/
            $bgcolor = $this->AllocateColor($im, $this->bgcolor[0], $this->bgcolor[1], $this->bgcolor[2]);
            imagefilledrectangle($im, 0 , 0, $this->formats[$index]['Width'], $this->formats[$index]['Height'], $bgcolor);
           
            /**
             * set background color transparent
             **/
            if ($this->bgcolor_transparent) {
                imagecolortransparent($im, $bgcolor);
            }
           
            /**
             * allocate pallete and get XOR image
             **/
            if (in_array($this->formats[$index]['BitCount'], array(1, 4, 8, 24))) {
                if ($this->formats[$index]['BitCount'] != 24) {
                    /**
                     * color pallete
                     **/
                    $c = array();
                    for ($i = 0; $i < $this->formats[$index]['ColorCount']; $i++) {
                        $c[$i] = $this->AllocateColor($im, $this->formats[$index]['colors'][$i]['blue'],
                                                           $this->formats[$index]['colors'][$i]['green'],
                                                           $this->formats[$index]['colors'][$i]['red'],
                                                           round($this->formats[$index]['colors'][$i]['reserved'] / 255 * 127));
                    }
                }
                /**
                 * XOR image
                 **/
                $width = $this->formats[$index]['Width'];
                if (($width % 32) > 0) {
                     $width += (32 - ($this->formats[$index]['Width'] % 32));
                }
                $offset = $this->formats[$index]['Width'] * $this->formats[$index]['Height'] * $this->formats[$index]['BitCount'] / 8;
                $total_bytes = ($width * $this->formats[$index]['Height']) / 8;
                $bits = '';
                $bytes = 0;
                $bytes_per_line = ($this->formats[$index]['Width'] / 8);
                $bytes_to_remove = (($width - $this->formats[$index]['Width']) / 8);
                for ($i = 0; $i < $total_bytes; $i++) {
                    $bits .= str_pad(decbin(ord($this->formats[$index]['data'][$offset + $i])), 8, '0', STR_PAD_LEFT);
                    $bytes++;
                    if ($bytes == $bytes_per_line) {
                        $i += $bytes_to_remove;
                        $bytes = 0;
                    }
                }
            }
            /**
             * paint each pixel depending on bit count
             **/
            switch ($this->formats[$index]['BitCount']) {
                case 32:
                    /**
                     * 32 bits: 4 bytes per pixel [ B | G | R | ALPHA ]
                     **/
                    $offset = 0;
                    for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
                        for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
                            $color = substr($this->formats[$index]['data'], $offset, 4);
                            if (ord($color[3]) > 0) {
                                $c = $this->AllocateColor($im, ord($color[2]),
                                                               ord($color[1]),
                                                               ord($color[0]),
                                                               127 - round(ord($color[3]) / 255 * 127));
                                imagesetpixel($im, $j, $i, $c);
                            }
                            $offset += 4;
                        }
                    }
                    break;
                case 24:
                    /**
                     * 24 bits: 3 bytes per pixel [ B | G | R ]
                     **/
                    $offset = 0;
                    $bitoffset = 0;
                    for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
                        for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
                            if ($bits[$bitoffset] == 0) {
                                $color = substr($this->formats[$index]['data'], $offset, 3);
                                $c = $this->AllocateColor($im, ord($color[2]), ord($color[1]), ord($color[0]));
                                imagesetpixel($im, $j, $i, $c);
                            }
                            $offset += 3;
                            $bitoffset++;
                        }
                    }
                    break;
                case 8:
                    /**
                     * 8 bits: 1 byte per pixel [ COLOR INDEX ]
                     **/
                    $offset = 0;
                    for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
                        for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
                            if ($bits[$offset] == 0) {
                                $color = ord(substr($this->formats[$index]['data'], $offset, 1));
                                imagesetpixel($im, $j, $i, $c[$color]);
                            }
                            $offset++;
                        }
                    }
                    break;
                case 4:
                    /**
                     * 4 bits: half byte/nibble per pixel [ COLOR INDEX ]
                     **/
                    $offset = 0;
                    $maskoffset = 0;
                    $leftbits = true;
                    for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
                        for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
                            if ($leftbits) {
                                $color = substr($this->formats[$index]['data'], $offset, 1);
                                $color = array(
                                    'High' => bindec(substr(decbin(ord($color)), 0, 4)),
                                    'Low' => bindec(substr(decbin(ord($color)), 4))
                                );
                                if ($bits[$maskoffset++] == 0) {
                                    imagesetpixel($im, $j, $i, $c[$color['High']]);
                                }
                                $leftbits = false;
                            } else {
                                if ($bits[$maskoffset++] == 0) {
                                    imagesetpixel($im, $j, $i, $c[$color['Low']]);
                                }
                                $offset++;
                                $leftbits = true;
                            }
                        }
                    }
                    break;
                case 1:
                    /**
                     * 1 bit: 1 bit per pixel (2 colors, usually black&white) [ COLOR INDEX ]
                     **/
                    $colorbits = '';
                    $total = strlen($this->formats[$index]['data']);
                    for ($i = 0; $i < $total; $i++) {
                        $colorbits .= str_pad(decbin(ord($this->formats[$index]['data'][$i])), 8, '0', STR_PAD_LEFT);
                    }
                   
                    $total = strlen($colorbits);
                    $offset = 0;
                    for ($i = $this->formats[$index]['Height'] - 1; $i >= 0; $i--) {
                        for ($j = 0; $j < $this->formats[$index]['Width']; $j++) {
                            if ($bits[$offset] == 0) {
                                imagesetpixel($im, $j, $i, $c[$colorbits[$offset]]);
                            }
                            $offset++;
                        }
                    }
                    break;
            }
           
            return $im;
        }
        /**
         * Ico::AllocateColor()
         * Allocate a color on $im resource. This function prevents
         * from allocating same colors on the same pallete. Instead
         * if it finds that the color is already allocated, it only
         * returns the index to that color.
         * It supports alpha channel.
         *
         * @param               resource    $im       Image resource
         * @param               integer     $red      Red component
         * @param               integer     $green    Green component
         * @param               integer     $blue     Blue component
         * @param   optional    integer     $alphpa   Alpha channel
         * @return              integer               Color index
         **/
        function AllocateColor(&$im, $red, $green, $blue, $alpha = 0) {
            $c = imagecolorexactalpha($im, $red, $green, $blue, $alpha);
            if ($c >= 0) {
                return $c;
            }
            return imagecolorallocatealpha($im, $red, $green, $blue, $alpha);
        }
    }
?>