images array by order of size, smallest to largest. This is the optimal sorting for icon files. countImages() Returns a count of how many images are present in the current floIcon object. floIcon public variables: $images Contains a numerically indexed array of floIconImage objects. $updated True if an image has been added since load or last formatICO, otherwise false. floIconImage public functions: getHeader() Returns an associative array containing the information from the ICO image header. getEntry() Returns an associative array containing the information from the ICO entry header. NOTE: If this icon image was created by php image resource, this may not have accurate information until saving from floIcon with the formatICO() function. Specifically, offset information will be inaccurate. getImageResource() Returns a php image resource. Same as floIcon's getImage() function. setImageResource($imageResource, $desiredBitCount = 1, $pngIfWidthExceeds = 48) Changes this icon image based on the passed image resource ($imageResource). It will automatically determine the bit count, but can be persuaded to increase that to $desiredBitCount if that value is greater than the determined bit count. NOTE: The image resource is saved by REFERRENCE. So, if you edit it then call getImageResource, the resource returned will be the same, editions and all. Destruction of the resource will cause a new resource to be created in getImageResource(). dealocateResource() This destroys the image resource variable, freeing up memory. The image will automatically be recreated when getImageResource is executed. */ class floIcon { /* * $images is an associative array of offset integer => floIconImage object */ var $images; // Array of floIconImage objects. var $updated = false; function floIcon() { $this->images = array(); } function countImages() { return count($this->images); } function getBestImage($height = 32, $width = 32) { $best = false; $bestEntry = array(); $secondBest = false; $secondBestEntry = array(); foreach ($this->images as $key => $image) { $entry = $image->getEntry(); $header = $image->getHeader(); if (!@$entry["BitCount"]) { $entry["BitCount"] = $header["BitCount"]; } if ($entry["Height"] == $height && $entry["Width"] == $width && $entry["BitCount"] == 32) { return $image->getImageResource(); } elseif ($entry["Height"] == $height && $entry["Width"] == $width && $entry["BitCount"] > min(4, @$bestEntry["BitCount"])) { $best = $image; $bestEntry = $entry; } elseif ( !$secondBest or $entry["Height"] >= $secondBestEntry["Height"] && $entry["Width"] >= $secondBestEntry["Width"] && $secondBestEntry["BitCount"] >= $secondBestEntry["BitCount"] and ( $entry["Height"] <= 64 && $entry["Height"] > $secondBestEntry["Height"] and $entry["Height"] > 64 && $entry["Height"] < $secondBestEntry["Height"] ) || ( $entry["Width"] <= 64 && $entry["Width"] > $secondBestEntry["Width"] and $entry["Width"] > 64 && $entry["Width"] < $secondBestEntry["Width"] ) || $secondBestEntry["BitCount"] > $secondBestEntry["BitCount"] ) { $secondBest = $image; $secondBestEntry = $entry; } } if ($best) { return $best->getImageResource(); } elseif ($secondBest) { if ($secondBestEntry["Width"] != $width || $secondBestEntry["Height"] != $height) { $imageResource = $secondBest->getImageResource(); $newImageResource = imagecreatetruecolor($width, $height); imagesavealpha($newImageResource, true); imagealphablending($newImageResource, false); imagecopyresampled($newImageResource, $imageResource, 0, 0, 0, 0, $width, $height, $secondBestEntry["Width"], $secondBestEntry["Height"]); $this->addImage($newImageResource, 32); return $newImageResource; } else { return $secondBest->getImageResource(); } } } /* * readICO merges the icon images from the file to the current list */ function readICO($file, $offset = 0) { if (file_exists($file) && filesize($file) > 0 && $filePointer = fopen($file, "r")) { fseek($filePointer, $offset); $header = unpack("SReserved/SType/SCount", fread($filePointer, 6)); for ($t = 0; $t < $header["Count"]; $t++) { $newImage = new floIconImage(); $newImage->readImageFromICO($filePointer, 6 + ($t * 16)); $this->images[] = $newImage; } fclose($filePointer); } } function sortImagesBySize() { usort($this->images, array("floIcon", "_cmpObj")); } function formatICO($offset = 0) { $this->updated = false; $output = ""; $output .= pack("SSS", 0, 1, count($this->images)); $output_images = ""; foreach ($this->images as $image) { $newImageOffset = $offset + // Whatever offset we've been given. 6 // Header. + (count($this->images) * 16) // Entries. + strlen($output_images); if ($newImageOffset > pow(256, 4) /* 4 bytes available for position */ ) { return false; } $output .= $image->formatEntryForIco($newImageOffset); // The images already in there. $output_images .= $image->formatImageForIco(); } return $output.$output_images; } function _cmpObj($a, $b) { $aSize = $a->getSize(); $bSize = $b->getSize(); if ($aSize == $bSize) { return 0; } return ($aSize > $bSize)?1:-1; } function addImage($imageResource, $desiredBitCount = 1, $pngIfWidthExceeds = 48) { $this->updated = true; $newImage = new floIconImage(); $newImage->setImageResource($imageResource, $desiredBitCount, $pngIfWidthExceeds); $this->images[] = $newImage; } function getImage($offset) { if (isset($this->images[$offset])) { return $this->images[$offset]->getImageResource(); } else { return false; } } /* * getSize computes the */ function getSize() { // Compute headers. $computedSize = 6; // Always 6 bytes. // Add image entry headers $computedSize += count($this->images) * 16; // Entry headers are always 16 bytes. foreach ($this->images as $image) { $computedSize += $image->getSize() + $image->getHeaderSize(); // getSize does not include the header. } } } class floIconImage { var $_imageResource = null; var $_entry = ""; var $_entryIconFormat = ""; var $_header = ""; var $_headerIconFormat = ""; var $_imageIconFormat = ""; // Includes palette and mask. function formatEntryForIco($offset) { // Format the entry, this has to be done here because we need the offset to get the full information. $this->_entry["FileOffset"] = $offset; $this->_entryIconFormat = pack("CCCCSSLL", $this->_entry["Width"]>=256?0:$this->_entry["Width"], $this->_entry["Height"]>=256?0:$this->_entry["Height"], $this->_entry["ColorCount"], $this->_entry["Reserved"], $this->_entry["Planes"], $this->_entry["BitCount"], $this->_entry["SizeInBytes"], $this->_entry["FileOffset"] ); return $this->_entryIconFormat; } function formatImageForIco() { // Format the entry, this has to be done here because we need the offset to get the full information. return ($this->_headerIconFormat.$this->_imageIconFormat); } // Will move $bitCount UP to $desiredBitCount if $bitCount is found to be less than it. function setImageResource($imageResource, $desiredBitCount = 1, $pngIfWidthExceeds = 48) { imagesavealpha($imageResource, true); imagealphablending($imageResource, false); $height = imagesy($imageResource); $width = imagesx($imageResource); // Parse resource to determine header and icon format // Find Palette information $is_32bit = false; // Start with an assumption and get proven wrong. $hasTransparency = 0; $blackColor = false; $bitCount = 0; $realPalette = array(); $realIndexPalette = array(); for ($x = 0; $x < $width && !$is_32bit; $x++) { for ($y = 0; $y < $height && !$is_32bit; $y++) { $colorIndex = imagecolorat($imageResource, $x, $y); $color = imagecolorsforindex($imageResource, $colorIndex); if ($color["alpha"] == 0) { // No point continuing if there's more than 256 colors or it's 32bit. if (count($realPalette) < 257 && !$is_32bit) { $inRealPalette = false; foreach($realPalette as $realPaletteKey => $realPaletteColor) { if ( $color["red"] == $realPaletteColor["red"] and $color["green"] == $realPaletteColor["green"] and $color["blue"] == $realPaletteColor["blue"] ) { $inRealPalette = $realPaletteKey; break; } } if ($inRealPalette === false) { $realIndexPalette[$colorIndex] = count($realPalette); if ( $blackColor === false and $color["red"] == 0 and $color["green"] == 0 and $color["blue"] == 0 ) { $blackColor = count($realPalette); } $realPalette[] = $color; } else { $realIndexPalette[$colorIndex] = $inRealPalette; } } } else { $hasTransparency = 1; } if ($color["alpha"] != 0 && $color["alpha"] != 127) { $is_32bit = true; } } } if ($is_32bit) { $colorCount = 0; $bitCount = 32; } else { if ($hasTransparency && $blackColor === false) { // We need a black color to facilitate transparency. Unfortunately, this can // increase the palette size by 1 if there's no other black color. $blackColor = count($realPalette); $color = array( "red" => 0, "blue" => 0, "green" => 0, "alpha" => 0 ); $realPalette[] = $color; } $colorCount = count($realPalette); if ($colorCount > 256 || $colorCount == 0) { $bitCount = 24; } elseif ($colorCount > 16) { $bitCount = 8; // 8 bit } elseif ($colorCount > 2) { $bitCount = 4; // 4 bit } else { $bitCount = 1; // 1 bit } if ($desiredBitCount > $bitCount) { $bitCount = $desiredBitCount; } switch ($bitCount) { case 24: $colorCount = 0; break; case 8: $colorCount = 256; break; case 4: $colorCount = 16; break; case 1: $colorCount = 2; break; } } // Create $this->_imageIconFormat... $this->_imageIconFormat = ""; if ($bitCount < 24) { $iconPalette = array(); // Save Palette foreach ($realIndexPalette as $colorIndex => $paletteIndex) { $color = $realPalette[$paletteIndex]; $this->_imageIconFormat .= pack("CCCC", $color["blue"], $color["green"], $color["red"], 0); } while (strlen($this->_imageIconFormat) < $colorCount * 4) { $this->_imageIconFormat .= pack("CCCC", 0, 0, 0, 0); } // Save Each Pixel as Palette Entry $byte = 0; // For $bitCount < 8 math $bitPosition = 0; // For $bitCount < 8 math for ($y = 0; $y < $height; $y++) { for ($x = 0; $x < $width; $x++) { $color = imagecolorat($imageResource, $x, $height-$y-1); if (isset($realIndexPalette[$color])) { $color = $realIndexPalette[$color]; } else { $color = $blackColor; } if ($bitCount < 8) { $bitPosition += $bitCount; $colorAdjusted = $color * pow(2, 8 - $bitPosition); $byte += $colorAdjusted; if ($bitPosition == 8) { $this->_imageIconFormat .= chr($byte); $bitPosition = 0; $byte = 0; } } else { $this->_imageIconFormat .= chr($color); } } // Each row ends with dumping the remaining bits and filling up to the 32bit line with 0's. if ($bitPosition) { $this->_imageIconFormat .= chr($byte); $bitPosition = 0; $byte = 0; } if (strlen($this->_imageIconFormat)%4) $this->_imageIconFormat .= str_repeat(chr(0), 4-(strlen($this->_imageIconFormat)%4)); } } else { // Save each pixel. for ($y = 0; $y < $height; $y++) { for ($x = 0; $x < $width; $x++) { $color = imagecolorat($imageResource, $x, $height-$y-1); $color = imagecolorsforindex($imageResource, $color); if ($bitCount == 24) { if ($color["alpha"]) { $this->_imageIconFormat .= pack("CCC", 0, 0, 0); } else { $this->_imageIconFormat .= pack("CCC", $color["blue"], $color["green"], $color["red"]); } } else { $color["alpha"] = round((127-$color["alpha"]) / 127 * 255); $this->_imageIconFormat .= pack("CCCC", $color["blue"], $color["green"], $color["red"], $color["alpha"]); } } if (strlen($this->_imageIconFormat)%4) $this->_imageIconFormat .= str_repeat(chr(0), 4-(strlen($this->_imageIconFormat)%4)); } } // save AND map (transparency) $byte = 0; // For $bitCount < 8 math $bitPosition = 0; // For $bitCount < 8 math for ($y = 0; $y < $height; $y++) { for ($x = 0; $x < $width; $x++) { if ($bitCount < 32) { $color = imagecolorat($imageResource, $x, $height-$y-1); $color = imagecolorsforindex($imageResource, $color); $color = $color["alpha"] == 127?1:0; } else { $color = 0; } $bitPosition += 1; $colorAdjusted = $color * pow(2, 8 - $bitPosition); $byte += $colorAdjusted; if ($bitPosition == 8) { $this->_imageIconFormat .= chr($byte); $bitPosition = 0; $byte = 0; } } // Each row ends with dumping the remaining bits and filling up to the 32bit line with 0's. if ($bitPosition) { $this->_imageIconFormat .= chr($byte); $bitPosition = 0; // For $bitCount < 8 math $byte = 0; } while (strlen($this->_imageIconFormat)%4) { $this->_imageIconFormat .= chr(0); } } if ($colorCount >= 256) { $colorCount = 0; } // Create header information... $this->_header = array( "Size" => 40, "Width" => $width, "Height" => $height*2, "Planes" => 1, "BitCount" => $bitCount, "Compression" => 0, "ImageSize" => strlen($this->_imageIconFormat), "XpixelsPerM" => 0, "YpixelsPerM" => 0, "ColorsUsed" => $colorCount, "ColorsImportant" => 0, ); $this->_headerIconFormat = pack("LLLSSLLLLLL", $this->_header["Size"], $this->_header["Width"], $this->_header["Height"], $this->_header["Planes"], $this->_header["BitCount"], $this->_header["Compression"], $this->_header["ImageSize"], $this->_header["XpixelsPerM"], $this->_header["YpixelsPerM"], $this->_header["ColorsUsed"], $this->_header["ColorsImportant"] ); $this->_entry = array( "Width" => $width, "Height" => $height, "ColorCount" => $colorCount, "Reserved" => 0, "Planes" => 1, "BitCount" => $bitCount, "SizeInBytes" => $this->_header["Size"] + $this->_header["ImageSize"], "FileOffset" => -1, ); $this->_entryIconFormat = ""; // This won't get set until it's needed with the offset. $this->_imageResource = $imageResource; // Make png if width exceeds limit for old ico style if ($width > $pngIfWidthExceeds) { // I wish there were a better way to get the info than this. If anyone needs a version that doesn't use OB, I can have one that creates a TMP file. ob_start(); imagepng($imageResource); $imageAsPng = ob_get_contents(); ob_end_clean(); $this->_headerIconFormat = ""; $this->_imageIconFormat = $imageAsPng; } } function _createImageResource() { if ($newImage = @imagecreatefromstring($this->_headerIconFormat.$this->_imageIconFormat)) { // Vista supports PNG. $this->_headerIconFormat = ""; $this->_imageIconFormat = $this->_headerIconFormat.$this->_imageIconFormat; imagesavealpha($newImage, true); imagealphablending($newImage, false); $this->_imageResource = $newImage; } elseif ($this->_entry["Height"] <= 1024 && $this->_entry["Width"] <= 1024) { $newImage = imagecreatetruecolor($this->_entry["Width"], $this->_entry["Height"]); imagesavealpha($newImage, true); imagealphablending($newImage, false); $readPosition = 0; $palette = array(); if ($this->_header["BitCount"] < 24) { // Read Palette for low bitcounts $colorsInPalette = $this->_header["ColorsUsed"]?$this->_header["ColorsUsed"]:$this->_entry["ColorCount"]; for ($t = 0; $t < pow(2, $this->_header["BitCount"]); $t++) { $blue = ord($this->_imageIconFormat[$readPosition++]); $green = ord($this->_imageIconFormat[$readPosition++]); $red = ord($this->_imageIconFormat[$readPosition++]); $readPosition++; // Unused "Reserved" value. $existingPaletteEntry = imagecolorexactalpha($newImage, $red, $green, $blue, 0); if ($existingPaletteEntry >= 0) { $palette[] = $existingPaletteEntry; } else { $palette[] = imagecolorallocatealpha($newImage, $red, $green, $blue, 0); } } // XOR for ($y = 0; $y < $this->_entry["Height"]; $y++) { $colors = array(); for ($x = 0; $x < $this->_entry["Width"]; $x++) { if ($this->_header["BitCount"] < 8) { $color = array_shift($colors); if (is_null($color)) { $byte = ord($this->_imageIconFormat[$readPosition++]); $tmp_color = 0; for ($t = 7; $t >= 0; $t--) { $bit_value = pow(2, $t); $bit = floor($byte / $bit_value); $byte = $byte - ($bit * $bit_value); $tmp_color += $bit * pow(2, $t%$this->_header["BitCount"]); if ($t%$this->_header["BitCount"] == 0) { array_push($colors, $tmp_color); $tmp_color = 0; } } $color = array_shift($colors); } } else { $color = ord($this->_imageIconFormat[$readPosition++]); } imagesetpixel($newImage, $x, $this->_entry["Height"]-$y-1, $palette[$color]) or die("can't set pixel"); } // All rows end on the 32 bit if ($readPosition%4) $readPosition += 4-($readPosition%4); } } else { // BitCount >= 24, No Palette. // marking position because some icons mark all pixels transparent when using an AND map. $markPosition = $readPosition; $retry = true; $ignoreAlpha = false; while ($retry) { $alphas = array(); $retry = false; for ($y = 0; $y < $this->_entry["Height"] and !$retry; $y++) { for ($x = 0; $x < $this->_entry["Width"] and !$retry; $x++) { $blue = ord($this->_imageIconFormat[$readPosition++]); $green = ord($this->_imageIconFormat[$readPosition++]); $red = ord($this->_imageIconFormat[$readPosition++]); if ($this->_header["BitCount"] < 32) { $alpha = 0; } elseif($ignoreAlpha) { $alpha = 0; $readPosition++; } else { $alpha = ord($this->_imageIconFormat[$readPosition++]); $alphas[$alpha] = $alpha; $alpha = 127-round($alpha/255*127); } $paletteEntry = imagecolorexactalpha($newImage, $red, $green, $blue, $alpha); if ($paletteEntry < 0) { $paletteEntry = imagecolorallocatealpha($newImage, $red, $green, $blue, $alpha); } imagesetpixel($newImage, $x, $this->_entry["Height"]-$y-1, $paletteEntry) or die("can't set pixel"); } if ($readPosition%4) $readPosition += 4-($readPosition%4); } if ($this->_header["BitCount"] == 32 && isset($alphas[0]) && count($alphas) == 1) { $retry = true; $readPosition = $markPosition; $ignoreAlpha = true; } } } // AND map if ($this->_header["BitCount"] < 32 || $ignoreAlpha) { // Bitcount == 32, No AND (if using alpha). $palette[-1] = imagecolorallocatealpha($newImage, 0, 0, 0, 127); imagecolortransparent($newImage, $palette[-1]); for ($y = 0; $y < $this->_entry["Height"]; $y++) { $colors = array(); for ($x = 0; $x < $this->_entry["Width"]; $x++) { $color = array_shift($colors); if (is_null($color)) { $byte = ord($this->_imageIconFormat[$readPosition++]); $tmp_color = 0; for ($t = 7; $t >= 0; $t--) { $bit_value = pow(2, $t); $bit = floor($byte / $bit_value); $byte = $byte - ($bit * $bit_value); array_push($colors, $bit); } $color = array_shift($colors); } if ($color) { imagesetpixel($newImage, $x, $this->_entry["Height"]-$y-1, $palette[-1]) or die("can't set pixel"); } } // All rows end on the 32 bit. if ($readPosition%4) $readPosition += 4-($readPosition%4); } } if ($this->_header["BitCount"] < 24) { imagetruecolortopalette($newImage, true, pow(2, $this->_header["BitCount"])); } } $this->_imageResource = $newImage; } // this function expects that $_entry, $_header and $_imageIconFormat have already been read, specifically from readImageFromICO. // Don't call this function except from there. function readImageFromICO($filePointer, $entryOffset) { $tmpPosition = ftell($filePointer); // So any other applications won't loose their position. // Get the entry. fseek($filePointer, $entryOffset); $this->_entryIconFormat = fread($filePointer, 16); $this->_entry = unpack("CWidth/CHeight/CColorCount/CReserved/SPlanes/SBitCount/LSizeInBytes/LFileOffset", $this->_entryIconFormat); // fox if ($this->_entry["SizeInBytes"] > 16384) $this->_entry["SizeInBytes"] = 16384; // Position the file pointer. fseek($filePointer, $this->_entry["FileOffset"]); // Get the header. $this->_headerIconFormat = fread($filePointer, 40); $this->_header = unpack("LSize/LWidth/LHeight/SPlanes/SBitCount/LCompression/LImageSize/LXpixelsPerM/LYpixelsPerM/LColorsUsed/LColorsImportant", $this->_headerIconFormat); // Get the image. $this->_imageIconFormat = @fread($filePointer, $this->_entry["SizeInBytes"] - strlen($this->_headerIconFormat)); fseek($filePointer, $tmpPosition); // So any other applications won't loose their position. if ($newImage = @imagecreatefromstring($this->_headerIconFormat.$this->_imageIconFormat)) { // This is a PNG, the supposed header information is useless. $this->_header = array ( "Size" => 0, "Width" => imagesx($newImage), "Height" => imagesy($newImage) * 2, "Planes" => 0, "BitCount" => 32, "Compression" => 0, "ImageSize" => strlen($this->_imageIconFormat), "XpixelsPerM" => 0, "YpixelsPerM" => 0, "ColorsUsed" => 0, "ColorsImportant" => 0, ); imagedestroy($newImage); } // Support for larger images requires entry marked as 0. if ($this->_entry["Width"] == 0) { $this->_entry["Width"] = $this->_header["Width"]; } if ($this->_entry["Height"] == 0) { $this->_entry["Height"] = $this->_header["Height"]/2; } } function getHeader() { return $this->_header; } function getEntry() { return $this->_entry; } function floIconImage() { } function getHeaderSize() { return strlen($this->_headerIconFormat); } function getSize() { return strlen($this->_imageIconFormat); } function getImageResource() { if (!$this->_imageResource) $this->_createImageResource(); return $this->_imageResource; } function dealocateResource() { @imagedestroy($this->_imageResource); $this->_imageResource = null; } } ?>