diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ef3eb52 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.php] +indent_size = 4 + +[*.json] +indent_size = 4 + +[composer.lock] +indent_size = 4 + +[phpunit.xml.dist] +indent_size = 4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09c3d74 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +/composer.lock +/phpunit/ +/phpunit.xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..eba105a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +dist: trusty +language: php +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 +cache: + directories: + - ./vendor/ +before_script: + - travis_retry composer install --no-interaction --no-suggest --prefer-source +script: + - composer run tests +matrix: + fast_finish: true diff --git a/class-php-ico.php b/class-php-ico.php index 2e9983e..a2102d7 100644 --- a/class-php-ico.php +++ b/class-php-ico.php @@ -7,258 +7,276 @@ Version 1.0.4 */ -class PHP_ICO { - /** - * Images in the BMP format. - * - * @var array - * @access private - */ - var $_images = array(); - - /** - * Flag to tell if the required functions exist. - * - * @var boolean - * @access private - */ - var $_has_requirements = false; - - - /** - * Constructor - Create a new ICO generator. - * - * If the constructor is not passed a file, a file will need to be supplied using the {@link PHP_ICO::add_image} - * function in order to generate an ICO file. - * - * @param string $file Optional. Path to the source image file. - * @param array $sizes Optional. An array of sizes (each size is an array with a width and height) that the source image should be rendered at in the generated ICO file. If sizes are not supplied, the size of the source image will be used. - */ - function __construct( $file = false, $sizes = array() ) { - $required_functions = array( - 'getimagesize', - 'imagecreatefromstring', - 'imagecreatetruecolor', - 'imagecolortransparent', - 'imagecolorallocatealpha', - 'imagealphablending', - 'imagesavealpha', - 'imagesx', - 'imagesy', - 'imagecopyresampled', - ); - - foreach ( $required_functions as $function ) { - if ( ! function_exists( $function ) ) { - trigger_error( "The PHP_ICO class was unable to find the $function function, which is part of the GD library. Ensure that the system has the GD library installed and that PHP has access to it through a PHP interface, such as PHP's GD module. Since this function was not found, the library will be unable to create ICO files." ); - return; - } - } - - $this->_has_requirements = true; - - - if ( false != $file ) - $this->add_image( $file, $sizes ); - } - - /** - * Add an image to the generator. - * - * This function adds a source image to the generator. It serves two main purposes: add a source image if one was - * not supplied to the constructor and to add additional source images so that different images can be supplied for - * different sized images in the resulting ICO file. For instance, a small source image can be used for the small - * resolutions while a larger source image can be used for large resolutions. - * - * @param string $file Path to the source image file. - * @param array $sizes Optional. An array of sizes (each size is an array with a width and height) that the source image should be rendered at in the generated ICO file. If sizes are not supplied, the size of the source image will be used. - * @return boolean true on success and false on failure. - */ - function add_image( $file, $sizes = array() ) { - if ( ! $this->_has_requirements ) - return false; - - if ( false === ( $im = $this->_load_image_file( $file ) ) ) - return false; - - - if ( empty( $sizes ) ) - $sizes = array( imagesx( $im ), imagesy( $im ) ); - - // If just a single size was passed, put it in array. - if ( ! is_array( $sizes[0] ) ) - $sizes = array( $sizes ); - - foreach ( (array) $sizes as $size ) { - list( $width, $height ) = $size; - - $new_im = imagecreatetruecolor( $width, $height ); - - imagecolortransparent( $new_im, imagecolorallocatealpha( $new_im, 0, 0, 0, 127 ) ); - imagealphablending( $new_im, false ); - imagesavealpha( $new_im, true ); - - $source_width = imagesx( $im ); - $source_height = imagesy( $im ); - - if ( false === imagecopyresampled( $new_im, $im, 0, 0, 0, 0, $width, $height, $source_width, $source_height ) ) - continue; - - $this->_add_image_data( $new_im ); - } - - return true; - } - - /** - * Write the ICO file data to a file path. - * - * @param string $file Path to save the ICO file data into. - * @return boolean true on success and false on failure. - */ - function save_ico( $file ) { - if ( ! $this->_has_requirements ) - return false; +class PHP_ICO +{ + /** + * Images in the BMP format. + * + * @var array + */ + private $_images = array(); + + /** + * Flag to tell if the required functions exist. + * + * @var boolean + */ + private $_has_requirements = false; + + + /** + * Constructor - Create a new ICO generator. + * + * If the constructor is not passed a file, a file will need to be supplied using the {@link PHP_ICO::add_image} + * function in order to generate an ICO file. + * + * @param string $file Optional. Path to the source image file. + * @param array $sizes Optional. An array of sizes (each size is an array with a width and height) that the source image should be rendered at in the generated ICO file. If sizes are not supplied, the size of the source image will be used. + */ + public function __construct($file = false, $sizes = array()) + { + $required_functions = array( + 'getimagesize', + 'imagecreatefromstring', + 'imagecreatetruecolor', + 'imagecolortransparent', + 'imagecolorallocatealpha', + 'imagealphablending', + 'imagesavealpha', + 'imagesx', + 'imagesy', + 'imagecopyresampled', + ); + + foreach ($required_functions as $function) { + if (! function_exists($function)) { + trigger_error("The PHP_ICO class was unable to find the $function function, which is part of the GD library. Ensure that the system has the GD library installed and that PHP has access to it through a PHP interface, such as PHP's GD module. Since this function was not found, the library will be unable to create ICO files."); + return; + } + } + + $this->_has_requirements = true; + + + if (false != $file) { + $this->add_image($file, $sizes); + } + } + + /** + * Add an image to the generator. + * + * This function adds a source image to the generator. It serves two main purposes: add a source image if one was + * not supplied to the constructor and to add additional source images so that different images can be supplied for + * different sized images in the resulting ICO file. For instance, a small source image can be used for the small + * resolutions while a larger source image can be used for large resolutions. + * + * @param string $file Path to the source image file. + * @param array $sizes Optional. An array of sizes (each size is an array with a width and height) that the source image should be rendered at in the generated ICO file. If sizes are not supplied, the size of the source image will be used. + * @return boolean true on success and false on failure. + */ + public function add_image($file, $sizes = array()) + { + if (! $this->_has_requirements) { + return false; + } + + if (false === ($im = $this->_load_image_file($file))) { + return false; + } + + + if (empty($sizes)) { + $sizes = array( imagesx($im), imagesy($im) ); + } + + // If just a single size was passed, put it in array. + if (! is_array($sizes[0])) { + $sizes = array( $sizes ); + } + + foreach ((array) $sizes as $size) { + list($width, $height) = $size; + + $new_im = imagecreatetruecolor($width, $height); + + imagecolortransparent($new_im, imagecolorallocatealpha($new_im, 0, 0, 0, 127)); + imagealphablending($new_im, false); + imagesavealpha($new_im, true); + + $source_width = imagesx($im); + $source_height = imagesy($im); + + if (false === imagecopyresampled($new_im, $im, 0, 0, 0, 0, $width, $height, $source_width, $source_height)) { + continue; + } + + $this->_add_image_data($new_im); + } + + return true; + } + + /** + * Write the ICO file data to a file path. + * + * @param string $file Path to save the ICO file data into. + * @return boolean true on success and false on failure. + */ + public function save_ico($file) + { + if (! $this->_has_requirements) { + return false; + } + + if (false === ($data = $this->_get_ico_data())) { + return false; + } - if ( false === ( $data = $this->_get_ico_data() ) ) - return false; + if (false === ($fh = fopen($file, 'w'))) { + return false; + } - if ( false === ( $fh = fopen( $file, 'w' ) ) ) - return false; + if (false === (fwrite($fh, $data))) { + fclose($fh); + return false; + } - if ( false === ( fwrite( $fh, $data ) ) ) { - fclose( $fh ); - return false; - } + fclose($fh); - fclose( $fh ); + return true; + } - return true; - } + /** + * Generate the final ICO data by creating a file header and adding the image data. + */ + private function _get_ico_data() + { + if (! is_array($this->_images) || empty($this->_images)) { + return false; + } - /** - * Generate the final ICO data by creating a file header and adding the image data. - * - * @access private - */ - function _get_ico_data() { - if ( ! is_array( $this->_images ) || empty( $this->_images ) ) - return false; + $data = pack('vvv', 0, 1, count($this->_images)); + $pixel_data = ''; - $data = pack( 'vvv', 0, 1, count( $this->_images ) ); - $pixel_data = ''; + $icon_dir_entry_size = 16; - $icon_dir_entry_size = 16; + $offset = 6 + ($icon_dir_entry_size * count($this->_images)); - $offset = 6 + ( $icon_dir_entry_size * count( $this->_images ) ); + foreach ($this->_images as $image) { + $data .= pack('CCCCvvVV', $image['width'], $image['height'], $image['color_palette_colors'], 0, 1, $image['bits_per_pixel'], $image['size'], $offset); + $pixel_data .= $image['data']; - foreach ( $this->_images as $image ) { - $data .= pack( 'CCCCvvVV', $image['width'], $image['height'], $image['color_palette_colors'], 0, 1, $image['bits_per_pixel'], $image['size'], $offset ); - $pixel_data .= $image['data']; + $offset += $image['size']; + } - $offset += $image['size']; - } + $data .= $pixel_data; + unset($pixel_data); - $data .= $pixel_data; - unset( $pixel_data ); + return $data; + } - return $data; - } + /** + * Take a GD image resource and change it into a raw BMP format. + */ + private function _add_image_data($im) + { + $width = imagesx($im); + $height = imagesy($im); - /** - * Take a GD image resource and change it into a raw BMP format. - * - * @access private - */ - function _add_image_data( $im ) { - $width = imagesx( $im ); - $height = imagesy( $im ); + $pixel_data = array(); - $pixel_data = array(); + $opacity_data = array(); + $current_opacity_val = 0; - $opacity_data = array(); - $current_opacity_val = 0; + for ($y = $height - 1; $y >= 0; $y--) { + for ($x = 0; $x < $width; $x++) { + $color = imagecolorat($im, $x, $y); - for ( $y = $height - 1; $y >= 0; $y-- ) { - for ( $x = 0; $x < $width; $x++ ) { - $color = imagecolorat( $im, $x, $y ); + $alpha = ($color & 0x7F000000) >> 24; + $alpha = (1 - ($alpha / 127)) * 255; - $alpha = ( $color & 0x7F000000 ) >> 24; - $alpha = ( 1 - ( $alpha / 127 ) ) * 255; + $color &= 0xFFFFFF; + $color |= 0xFF000000 & ($alpha << 24); - $color &= 0xFFFFFF; - $color |= 0xFF000000 & ( $alpha << 24 ); + $pixel_data[] = $color; - $pixel_data[] = $color; + $opacity = ($alpha <= 127) ? 1 : 0; - $opacity = ( $alpha <= 127 ) ? 1 : 0; + $current_opacity_val = ($current_opacity_val << 1) | $opacity; - $current_opacity_val = ( $current_opacity_val << 1 ) | $opacity; + if ((($x + 1) % 32) == 0) { + $opacity_data[] = $current_opacity_val; + $current_opacity_val = 0; + } + } - if ( ( ( $x + 1 ) % 32 ) == 0 ) { - $opacity_data[] = $current_opacity_val; - $current_opacity_val = 0; - } - } + if (($x % 32) > 0) { + while (($x++ % 32) > 0) { + $current_opacity_val = $current_opacity_val << 1; + } - if ( ( $x % 32 ) > 0 ) { - while ( ( $x++ % 32 ) > 0 ) - $current_opacity_val = $current_opacity_val << 1; + $opacity_data[] = $current_opacity_val; + $current_opacity_val = 0; + } + } - $opacity_data[] = $current_opacity_val; - $current_opacity_val = 0; - } - } + $image_header_size = 40; + $color_mask_size = $width * $height * 4; + $opacity_mask_size = (ceil($width / 32) * 4) * $height; - $image_header_size = 40; - $color_mask_size = $width * $height * 4; - $opacity_mask_size = ( ceil( $width / 32 ) * 4 ) * $height; + $data = pack('VVVvvVVVVVV', 40, $width, ($height * 2), 1, 32, 0, 0, 0, 0, 0, 0); - $data = pack( 'VVVvvVVVVVV', 40, $width, ( $height * 2 ), 1, 32, 0, 0, 0, 0, 0, 0 ); + foreach ($pixel_data as $color) { + $data .= pack('V', $color); + } - foreach ( $pixel_data as $color ) - $data .= pack( 'V', $color ); + foreach ($opacity_data as $opacity) { + $data .= pack('N', $opacity); + } - foreach ( $opacity_data as $opacity ) - $data .= pack( 'N', $opacity ); + $image = array( + 'width' => $width, + 'height' => $height, + 'color_palette_colors' => 0, + 'bits_per_pixel' => 32, + 'size' => $image_header_size + $color_mask_size + $opacity_mask_size, + 'data' => $data, + ); - $image = array( - 'width' => $width, - 'height' => $height, - 'color_palette_colors' => 0, - 'bits_per_pixel' => 32, - 'size' => $image_header_size + $color_mask_size + $opacity_mask_size, - 'data' => $data, - ); + $this->_images[] = $image; + } - $this->_images[] = $image; - } + /** + * Read in the source image file and convert it into a GD image resource. + */ + private function _load_image_file($file) + { + if (!is_string($file) || empty($file)) { + return false; + } + // Run a cheap check to verify that it is an image file. + if (false === ($size = getimagesize($file))) { + return false; + } - /** - * Read in the source image file and convert it into a GD image resource. - * - * @access private - */ - function _load_image_file( $file ) { - // Run a cheap check to verify that it is an image file. - if ( false === ( $size = getimagesize( $file ) ) ) - return false; + if (false === ($file_data = file_get_contents($file))) { + return false; + } - if ( false === ( $file_data = file_get_contents( $file ) ) ) - return false; + if (false === ($im = imagecreatefromstring($file_data))) { + return false; + } - if ( false === ( $im = imagecreatefromstring( $file_data ) ) ) - return false; + unset($file_data); - unset( $file_data ); - - return $im; - } + return $im; + } } diff --git a/composer.json b/composer.json index 71d4a93..14aa203 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,10 @@ "role": "Developer" } ], + "config": { + "optimize-autoloader": true, + "sort-packages": true + }, "support": { "issues": "https://github.com/chrisbliss18/php-ico/issues", "source": "https://github.com/chrisbliss18/php-ico" @@ -24,5 +28,20 @@ "classmap": [ "class-php-ico.php" ] + }, + "autoload-dev": { + "psr-4": { + "PHP_ICO\\Tests\\": "tests/" + } + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^1.12", + "phpunit/phpunit": "^4.8||^5.6" + }, + "scripts": { + "tests" : [ + "php-cs-fixer fix --verbose --level=psr2 class-php-ico.php --dry-run", + "phpunit" + ] } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..f456179 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,35 @@ + + + + + ./tests + + + + + ./class-php-ico.php + + + + + + diff --git a/tests/OutputIcoTest.php b/tests/OutputIcoTest.php new file mode 100644 index 0000000..3f57299 --- /dev/null +++ b/tests/OutputIcoTest.php @@ -0,0 +1,141 @@ +assertTrue(is_file($file)); + $this->assertTrue(is_readable($file)); + $this->assertTrue(is_file($file . '.ico')); + $this->assertTrue(is_readable($file . '.ico')); + + $ico = new PHP_ICO($file, $sizes); + $outputToHere = tempnam(sys_get_temp_dir(), 'PHP_ICO_tests'); + $this->assertTrue($ico->save_ico($outputToHere)); + $this->assertSame(sha1_file($file . '.ico'), sha1_file($outputToHere)); + unlink($outputToHere); + } + + /** + * @dataProvider badAddImageSingleProvider_invalidFile + */ + public function testAddImageBadFiles($file, $sizes) { + $ico = new PHP_ICO(); + $this->assertFalse($ico->add_image($file, $sizes)); + } + + /** + * @dataProvider badSaveIcoProvider_GetIcoDataReturnsFalse + */ + public function testSaveIcoBadData($arrayOfFilesAndSizes) + { + $ico = new PHP_ICO(); + foreach ($arrayOfFilesAndSizes as $file => $sizes) + { + $ico->add_image($file, $sizes); + } + $outputToHere = tempnam(sys_get_temp_dir(), 'PHP_ICO_tests'); + $this->assertFalse($ico->save_ico($outputToHere)); + unlink($outputToHere); + } +} diff --git a/tests/test-ico-1.gif b/tests/test-ico-1.gif new file mode 100644 index 0000000..9c0a951 Binary files /dev/null and b/tests/test-ico-1.gif differ diff --git a/tests/test-ico-1.gif.ico b/tests/test-ico-1.gif.ico new file mode 100644 index 0000000..5b3d22a Binary files /dev/null and b/tests/test-ico-1.gif.ico differ diff --git a/tests/test-ico-1.jpg b/tests/test-ico-1.jpg new file mode 100644 index 0000000..e21c5cc Binary files /dev/null and b/tests/test-ico-1.jpg differ diff --git a/tests/test-ico-1.jpg.ico b/tests/test-ico-1.jpg.ico new file mode 100644 index 0000000..d2e2e83 Binary files /dev/null and b/tests/test-ico-1.jpg.ico differ diff --git a/tests/test-ico-1.png b/tests/test-ico-1.png new file mode 100644 index 0000000..f110ae2 Binary files /dev/null and b/tests/test-ico-1.png differ diff --git a/tests/test-ico-1.png.ico b/tests/test-ico-1.png.ico new file mode 100644 index 0000000..5b3d22a Binary files /dev/null and b/tests/test-ico-1.png.ico differ diff --git a/tests/test-ico-1.xcf b/tests/test-ico-1.xcf new file mode 100644 index 0000000..54104cb Binary files /dev/null and b/tests/test-ico-1.xcf differ