1<?php 2 3/* 4Copyright 2011-2016 Chris Jean & iThemes 5Licensed under GPLv2 or above 6*/ 7 8namespace splitbrain\phpico; 9 10class PhpIco 11{ 12 /** 13 * Images in the BMP format. 14 * 15 * @var array 16 */ 17 private $images = array(); 18 19 /** 20 * Constructor - Create a new ICO generator. 21 * 22 * If the constructor is not passed a file, a file will need to be supplied using the {@link PHP_ICO::add_image} 23 * function in order to generate an ICO file. 24 * 25 * @param string $file Optional. Path to the source image file. 26 * @param array $sizes Optional. An array of sizes (each size is an array with a width and height) 27 * that the source image should be rendered at in the generated ICO file. 28 * If sizes are not supplied, the size of the source image will be used. 29 */ 30 public function __construct($file = false, $sizes = array()) 31 { 32 $required_functions = array( 33 'getimagesize', 34 'imagecreatefromstring', 35 'imagecreatetruecolor', 36 'imagecolortransparent', 37 'imagecolorallocatealpha', 38 'imagealphablending', 39 'imagesavealpha', 40 'imagesx', 41 'imagesy', 42 'imagecopyresampled', 43 ); 44 45 foreach ($required_functions as $function) { 46 if (!function_exists($function)) { 47 throw new \RuntimeException( 48 "The PhpIco class was unable to find the $function function, which is part of the GD library. ". 49 'Ensure that the system has the GD library installed and that PHP has access to it through a PHP '. 50 'interface, such as PHP\'s GD module. Since this function was not found, the library will be '. 51 'unable to create ICO files.' 52 ); 53 } 54 } 55 56 if ($file) { 57 $this->addImage($file, $sizes); 58 } 59 } 60 61 /** 62 * Add an image to the generator. 63 * 64 * This function adds a source image to the generator. It serves two main purposes: add a source image if one was 65 * not supplied to the constructor and to add additional source images so that different images can be supplied for 66 * different sized images in the resulting ICO file. For instance, a small source image can be used for the small 67 * resolutions while a larger source image can be used for large resolutions. 68 * 69 * @param string $file Path to the source image file. 70 * @param array $sizes Optional. An array of sizes (each size is an array with a width and height) that the source 71 * image should be rendered at in the generated ICO file. If sizes are not supplied, the size 72 * of the source image will be used. 73 * @return boolean true on success and false on failure. 74 */ 75 public function addImage($file, $sizes = array()) 76 { 77 if (false === ($im = $this->loadImageFile($file))) { 78 return false; 79 } 80 81 82 if (empty($sizes)) { 83 $sizes = array(imagesx($im), imagesy($im)); 84 } 85 86 // If just a single size was passed, put it in array. 87 if (!is_array($sizes[0])) { 88 $sizes = array($sizes); 89 } 90 91 foreach ((array)$sizes as $size) { 92 list($width, $height) = $size; 93 94 $new_im = imagecreatetruecolor($width, $height); 95 96 imagecolortransparent($new_im, imagecolorallocatealpha($new_im, 0, 0, 0, 127)); 97 imagealphablending($new_im, false); 98 imagesavealpha($new_im, true); 99 100 $source_width = imagesx($im); 101 $source_height = imagesy($im); 102 103 if (false === imagecopyresampled( 104 $new_im, 105 $im, 106 0, 107 0, 108 0, 109 0, 110 $width, 111 $height, 112 $source_width, 113 $source_height) 114 ) { 115 continue; 116 } 117 118 $this->addImageData($new_im); 119 } 120 121 return true; 122 } 123 124 /** 125 * Write the ICO file data to a file path. 126 * 127 * @param string $file Path to save the ICO file data into. 128 * @return boolean true on success and false on failure. 129 */ 130 public function saveIco($file) 131 { 132 if (false === ($data = $this->getIcoData())) { 133 return false; 134 } 135 136 if (false === ($fh = fopen($file, 'w'))) { 137 return false; 138 } 139 140 if (false === (fwrite($fh, $data))) { 141 fclose($fh); 142 return false; 143 } 144 145 fclose($fh); 146 147 return true; 148 } 149 150 /** 151 * Generate the final ICO data by creating a file header and adding the image data. 152 */ 153 protected function getIcoData() 154 { 155 if (!is_array($this->images) || empty($this->images)) { 156 return false; 157 } 158 159 160 $data = pack('vvv', 0, 1, count($this->images)); 161 $pixel_data = ''; 162 163 $icon_dir_entry_size = 16; 164 165 $offset = 6 + ($icon_dir_entry_size * count($this->images)); 166 167 foreach ($this->images as $image) { 168 $data .= pack( 169 'CCCCvvVV', 170 $image['width'], 171 $image['height'], 172 $image['color_palette_colors'], 173 0, 174 1, 175 $image['bits_per_pixel'], 176 $image['size'], 177 $offset 178 ); 179 $pixel_data .= $image['data']; 180 181 $offset += $image['size']; 182 } 183 184 $data .= $pixel_data; 185 unset($pixel_data); 186 187 188 return $data; 189 } 190 191 /** 192 * Take a GD image resource and change it into a raw BMP format. 193 */ 194 protected function addImageData($im) 195 { 196 $width = imagesx($im); 197 $height = imagesy($im); 198 199 200 $pixel_data = array(); 201 202 $opacity_data = array(); 203 $current_opacity_val = 0; 204 205 for ($y = $height - 1; $y >= 0; $y--) { 206 for ($x = 0; $x < $width; $x++) { 207 $color = imagecolorat($im, $x, $y); 208 209 $alpha = ($color & 0x7F000000) >> 24; 210 $alpha = (1 - ($alpha / 127)) * 255; 211 212 $color &= 0xFFFFFF; 213 $color |= 0xFF000000 & ($alpha << 24); 214 215 $pixel_data[] = $color; 216 217 218 $opacity = ($alpha <= 127) ? 1 : 0; 219 220 $current_opacity_val = ($current_opacity_val << 1) | $opacity; 221 222 if ((($x + 1) % 32) == 0) { 223 $opacity_data[] = $current_opacity_val; 224 $current_opacity_val = 0; 225 } 226 } 227 228 if (($x % 32) > 0) { 229 while (($x++ % 32) > 0) { 230 $current_opacity_val = $current_opacity_val << 1; 231 } 232 233 $opacity_data[] = $current_opacity_val; 234 $current_opacity_val = 0; 235 } 236 } 237 238 $image_header_size = 40; 239 $color_mask_size = $width * $height * 4; 240 $opacity_mask_size = (ceil($width / 32) * 4) * $height; 241 242 243 $data = pack('VVVvvVVVVVV', 40, $width, ($height * 2), 1, 32, 0, 0, 0, 0, 0, 0); 244 245 foreach ($pixel_data as $color) { 246 $data .= pack('V', $color); 247 } 248 249 foreach ($opacity_data as $opacity) { 250 $data .= pack('N', $opacity); 251 } 252 253 254 $image = array( 255 'width' => $width, 256 'height' => $height, 257 'color_palette_colors' => 0, 258 'bits_per_pixel' => 32, 259 'size' => $image_header_size + $color_mask_size + $opacity_mask_size, 260 'data' => $data, 261 ); 262 263 $this->images[] = $image; 264 } 265 266 /** 267 * Read in the source image file and convert it into a GD image resource. 268 */ 269 protected function loadImageFile($file) 270 { 271 if (!is_string($file) || empty($file)) { 272 return false; 273 } 274 // Run a cheap check to verify that it is an image file. 275 if (false === ($size = getimagesize($file))) { 276 return false; 277 } 278 279 if (false === ($file_data = file_get_contents($file))) { 280 return false; 281 } 282 283 if (false === ($im = imagecreatefromstring($file_data))) { 284 return false; 285 } 286 287 unset($file_data); 288 289 290 return $im; 291 } 292} 293