diff --git a/.travis.yml b/.travis.yml index 26d4873..a97aa99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: php php: + - 5.2 - 5.3 - 5.4 - 5.5 @@ -9,6 +10,8 @@ php: env: - WP_VERSION=latest WP_MULTISITE=0 - WP_VERSION=latest WP_MULTISITE=1 + - WP_VERSION=4.2 WP_MULTISITE=0 + - WP_VERSION=4.2 WP_MULTISITE=1 - WP_VERSION=4.1 WP_MULTISITE=0 - WP_VERSION=4.1 WP_MULTISITE=1 - WP_VERSION=4.0 WP_MULTISITE=0 @@ -21,3 +24,5 @@ script: phpunit notifications: email: false + +sudo: false diff --git a/class-respimg.php b/class-respimg.php index 120fc52..d645a9c 100644 --- a/class-respimg.php +++ b/class-respimg.php @@ -1,245 +1,239 @@ + * + * @package wp-respimg + * @version 0.0.1 + */ + +/** + * An Imagick extension to provide better (higher quality, lower file size) image resizes. + * + * This class extends Imagick () based on + * research into optimal image resizing techniques (). + * + * Using these methods with their default settings should provide image resizing that is + * visually indistinguishable from Photoshop’s “Save for Web…”, but at lower file sizes. + * + * @author David Newton + * @copyright 2015 David Newton + * @license https://raw.githubusercontent.com/nwtn/php-respimg/master/LICENSE MIT + * @version 1.0.0 + */ + +class Respimg extends Imagick { + /** - * hacked up version of php-respimg + * Resizes the image using smart defaults for high quality and low file size. + * + * This function is basically equivalent to: + * + * $optim == true: `mogrify -path OUTPUT_PATH -filter Triangle -define filter:support=2.0 -thumbnail OUTPUT_WIDTH -unsharp 0.25x0.08+8.3+0.045 -dither None -posterize 136 -quality 82 -define jpeg:fancy-upsampling=off -define png:compression-filter=5 -define png:compression-level=9 -define png:compression-strategy=1 -define png:exclude-chunk=all -interlace none -colorspace sRGB INPUT_PATH` + * + * $optim == false: `mogrify -path OUTPUT_PATH -filter Triangle -define filter:support=2.0 -thumbnail OUTPUT_WIDTH -unsharp 0.25x0.25+8+0.065 -dither None -posterize 136 -quality 82 -define jpeg:fancy-upsampling=off -define png:compression-filter=5 -define png:compression-level=9 -define png:compression-strategy=1 -define png:exclude-chunk=all -interlace none -colorspace sRGB -strip INPUT_PATH` + * + * @access public * - * @package wp-respimg - * @version 0.0.1 + * @param integer $columns The number of columns in the output image. 0 = maintain aspect ratio based on $rows. + * @param integer $rows The number of rows in the output image. 0 = maintain aspect ratio based on $columns. + * @param bool $optim Whether you intend to perform optimization on the resulting image. Note that setting this to `true` doesn’t actually perform any optimization. */ - if (class_exists('Imagick')) { - class RespimgImagick extends Imagick { } - } else { - class RespimgImagick { } + public function smartResize( $columns, $rows, $optim = false ) { + + $this->setOption( 'filter:support', '2.0' ); + $this->thumbnailImage( $columns, $rows, false, false, Imagick::FILTER_TRIANGLE ); + if ( $optim ) { + $this->unsharpMaskImage( 0.25, 0.08, 8.3, 0.045 ); + } else { + $this->unsharpMaskImage( 0.25, 0.25, 8, 0.065 ); + } + $this->posterizeImage( 136, false ); + $this->setImageCompressionQuality( 82 ); + $this->setOption( 'jpeg:fancy-upsampling', 'off' ); + $this->setOption( 'png:compression-filter', '5' ); + $this->setOption( 'png:compression-level', '9' ); + $this->setOption( 'png:compression-strategy', '1' ); + $this->setOption( 'png:exclude-chunk', 'all' ); + $this->setInterlaceScheme( Imagick::INTERLACE_NO ); + $this->setColorspace( Imagick::COLORSPACE_SRGB ); + if ( ! $optim ) { + $this->stripImage(); + } + } /** - * An Imagick extension to provide better (higher quality, lower file size) image resizes. + * Changes the size of an image to the given dimensions and removes any associated profiles. + * + * `thumbnailImage` changes the size of an image to the given dimensions and + * removes any associated profiles. The goal is to produce small low cost + * thumbnail images suited for display on the Web. * - * This class extends Imagick () based on - * research into optimal image resizing techniques (). + * With the original Imagick thumbnailImage implementation, there is no way to choose a + * resampling filter. This class recreates Imagick’s C implementation and adds this + * additional feature. * - * Using these methods with their default settings should provide image resizing that is - * visually indistinguishable from Photoshop’s “Save for Web…”, but at lower file sizes. + * Note: has been filed for this issue. * - * @author David Newton - * @copyright 2015 David Newton - * @license https://raw.githubusercontent.com/nwtn/php-respimg/master/LICENSE MIT - * @version 1.0.0 + * @access public + * + * @param integer $columns The number of columns in the output image. 0 = maintain aspect ratio based on $rows. + * @param integer $rows The number of rows in the output image. 0 = maintain aspect ratio based on $columns. + * @param bool $bestfit Treat $columns and $rows as a bounding box in which to fit the image. + * @param bool $fill Fill in the bounding box with the background colour. + * @param integer $filter The resampling filter to use. Refer to the list of filter constants at . + * + * @return bool Indicates whether the operation was performed successfully. */ - class Respimg extends RespimgImagick { - - /** - * Resizes the image using smart defaults for high quality and low file size. - * - * This function is basically equivalent to: - * - * $optim == true: `mogrify -path OUTPUT_PATH -filter Triangle -define filter:support=2.0 -thumbnail OUTPUT_WIDTH -unsharp 0.25x0.08+8.3+0.045 -dither None -posterize 136 -quality 82 -define jpeg:fancy-upsampling=off -define png:compression-filter=5 -define png:compression-level=9 -define png:compression-strategy=1 -define png:exclude-chunk=all -interlace none -colorspace sRGB INPUT_PATH` - * - * $optim == false: `mogrify -path OUTPUT_PATH -filter Triangle -define filter:support=2.0 -thumbnail OUTPUT_WIDTH -unsharp 0.25x0.25+8+0.065 -dither None -posterize 136 -quality 82 -define jpeg:fancy-upsampling=off -define png:compression-filter=5 -define png:compression-level=9 -define png:compression-strategy=1 -define png:exclude-chunk=all -interlace none -colorspace sRGB -strip INPUT_PATH` - * - * @access public - * - * @param integer $columns The number of columns in the output image. 0 = maintain aspect ratio based on $rows. - * @param integer $rows The number of rows in the output image. 0 = maintain aspect ratio based on $columns. - * @param bool $optim Whether you intend to perform optimization on the resulting image. Note that setting this to `true` doesn’t actually perform any optimization. - */ - - public function smartResize($columns, $rows, $optim = false) { - - $this->setOption('filter:support', '2.0'); - $this->thumbnailImage($columns, $rows, false, false, Imagick::FILTER_TRIANGLE); - if ($optim) { - $this->unsharpMaskImage(0.25, 0.08, 8.3, 0.045); - } else { - $this->unsharpMaskImage(0.25, 0.25, 8, 0.065); - } - $this->posterizeImage(136, false); - $this->setImageCompressionQuality(82); - $this->setOption('jpeg:fancy-upsampling', 'off'); - $this->setOption('png:compression-filter', '5'); - $this->setOption('png:compression-level', '9'); - $this->setOption('png:compression-strategy', '1'); - $this->setOption('png:exclude-chunk', 'all'); - $this->setInterlaceScheme(Imagick::INTERLACE_NO); - $this->setColorspace(Imagick::COLORSPACE_SRGB); - if (!$optim) { - $this->stripImage(); - } + public function thumbnailImage( $columns, $rows, $bestfit = false, $fill = false, $filter = Imagick::FILTER_TRIANGLE ) { + + // sample factor; defined in original ImageMagick thumbnailImage function + // the scale to which the image should be resized using the `sample` function + $SampleFactor = 5; + + // filter whitelist + $filters = array( + Imagick::FILTER_POINT, + Imagick::FILTER_BOX, + Imagick::FILTER_TRIANGLE, + Imagick::FILTER_HERMITE, + Imagick::FILTER_HANNING, + Imagick::FILTER_HAMMING, + Imagick::FILTER_BLACKMAN, + Imagick::FILTER_GAUSSIAN, + Imagick::FILTER_QUADRATIC, + Imagick::FILTER_CUBIC, + Imagick::FILTER_CATROM, + Imagick::FILTER_MITCHELL, + Imagick::FILTER_LANCZOS, + Imagick::FILTER_BESSEL, + Imagick::FILTER_SINC + ); + + // Parse parameters given to function + $columns = (double) $columns; + $rows = (double) $rows; + $bestfit = (bool) $bestfit; + $fill = (bool) $fill; + + // We can’t resize to (0,0) + if ( $rows < 1 && $columns < 1 ) { + return false; + } + // Set a default filter if an acceptable one wasn’t passed + if ( ! in_array( $filter, $filters ) ) { + $filter = Imagick::FILTER_TRIANGLE; } + // figure out the output width and height + $width = (double) $this->getImageWidth(); + $height = (double) $this->getImageHeight(); + $new_width = $columns; + $new_height = $rows; + + $x_factor = $columns / $width; + $y_factor = $rows / $height; + if ( $rows < 1 ) { + $new_height = round( $x_factor * $height ); + } elseif ( $columns < 1 ) { + $new_width = round( $y_factor * $width ); + } - /** - * Changes the size of an image to the given dimensions and removes any associated profiles. - * - * `thumbnailImage` changes the size of an image to the given dimensions and - * removes any associated profiles. The goal is to produce small low cost - * thumbnail images suited for display on the Web. - * - * With the original Imagick thumbnailImage implementation, there is no way to choose a - * resampling filter. This class recreates Imagick’s C implementation and adds this - * additional feature. - * - * Note: has been filed for this issue. - * - * @access public - * - * @param integer $columns The number of columns in the output image. 0 = maintain aspect ratio based on $rows. - * @param integer $rows The number of rows in the output image. 0 = maintain aspect ratio based on $columns. - * @param bool $bestfit Treat $columns and $rows as a bounding box in which to fit the image. - * @param bool $fill Fill in the bounding box with the background colour. - * @param integer $filter The resampling filter to use. Refer to the list of filter constants at . - * - * @return bool Indicates whether the operation was performed successfully. - */ - - public function thumbnailImage($columns, $rows, $bestfit = false, $fill = false, $filter = Imagick::FILTER_TRIANGLE) { - - // sample factor; defined in original ImageMagick thumbnailImage function - // the scale to which the image should be resized using the `sample` function - $SampleFactor = 5; - - // filter whitelist - $filters = array( - Imagick::FILTER_POINT, - Imagick::FILTER_BOX, - Imagick::FILTER_TRIANGLE, - Imagick::FILTER_HERMITE, - Imagick::FILTER_HANNING, - Imagick::FILTER_HAMMING, - Imagick::FILTER_BLACKMAN, - Imagick::FILTER_GAUSSIAN, - Imagick::FILTER_QUADRATIC, - Imagick::FILTER_CUBIC, - Imagick::FILTER_CATROM, - Imagick::FILTER_MITCHELL, - Imagick::FILTER_LANCZOS, - Imagick::FILTER_BESSEL, - Imagick::FILTER_SINC - ); - - // Parse parameters given to function - $columns = (double) ($columns); - $rows = (double) ($rows); - $bestfit = (bool) $bestfit; - $fill = (bool) $fill; - - // We can’t resize to (0,0) - if ($rows < 1 && $columns < 1) { - return false; - } + // if bestfit is true, the new_width/new_height of the image will be different than + // the columns/rows parameters; those will define a bounding box in which the image will be fit + if ( $bestfit && $x_factor > $y_factor ) { + $x_factor = $y_factor; + $new_width = round( $y_factor * $width ); + } elseif ( $bestfit && $y_factor > $x_factor ) { + $y_factor = $x_factor; + $new_height = round( $x_factor * $height ); + } + if ( $new_width < 1 ) { + $new_width = 1; + } + if ( $new_height < 1 ) { + $new_height = 1; + } - // Set a default filter if an acceptable one wasn’t passed - if (!in_array($filter, $filters)) { - $filter = Imagick::FILTER_TRIANGLE; - } + // if we’re resizing the image to more than about 1/3 it’s original size + // then just use the resize function + if ( ( $x_factor * $y_factor ) > 0.1 ) { + $this->resizeImage( $new_width, $new_height, $filter, 1 ); - // figure out the output width and height - $width = (double) $this->getImageWidth(); - $height = (double) $this->getImageHeight(); - $new_width = $columns; - $new_height = $rows; - - $x_factor = $columns / $width; - $y_factor = $rows / $height; - if ($rows < 1) { - $new_height = round($x_factor * $height); - } elseif ($columns < 1) { - $new_width = round($y_factor * $width); - } + // if we’d be using sample to scale to smaller than 128x128, just use resize + } elseif ( ( ( $SampleFactor * $new_width ) < 128) || ( ( $SampleFactor * $new_height ) < 128 ) ) { + $this->resizeImage( $new_width, $new_height, $filter, 1 ); - // if bestfit is true, the new_width/new_height of the image will be different than - // the columns/rows parameters; those will define a bounding box in which the image will be fit - if ($bestfit && $x_factor > $y_factor) { - $x_factor = $y_factor; - $new_width = round($y_factor * $width); - } elseif ($bestfit && $y_factor > $x_factor) { - $y_factor = $x_factor; - $new_height = round($x_factor * $height); - } - if ($new_width < 1) { - $new_width = 1; - } - if ($new_height < 1) { - $new_height = 1; - } + // otherwise, use sample first, then resize + } else { + $this->sampleImage( $SampleFactor * $new_width, $SampleFactor * $new_height ); + $this->resizeImage( $new_width, $new_height, $filter, 1 ); + } - // if we’re resizing the image to more than about 1/3 it’s original size - // then just use the resize function - if (($x_factor * $y_factor) > 0.1) { - $this->resizeImage($new_width, $new_height, $filter, 1); + // if the alpha channel is not defined, make it opaque + if ( $this->getImageAlphaChannel() == Imagick::ALPHACHANNEL_UNDEFINED ) { + $this->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE ); + } - // if we’d be using sample to scale to smaller than 128x128, just use resize - } elseif ((($SampleFactor * $new_width) < 128) || (($SampleFactor * $new_height) < 128)) { - $this->resizeImage($new_width, $new_height, $filter, 1); + // set the image’s bit depth to 8 bits + $this->setImageDepth( 8 ); - // otherwise, use sample first, then resize - } else { - $this->sampleImage($SampleFactor * $new_width, $SampleFactor * $new_height); - $this->resizeImage($new_width, $new_height, $filter, 1); - } + // turn off interlacing + $this->setInterlaceScheme( Imagick::INTERLACE_NO ); - // if the alpha channel is not defined, make it opaque - if ($this->getImageAlphaChannel() == Imagick::ALPHACHANNEL_UNDEFINED) { - $this->setImageAlphaChannel(Imagick::ALPHACHANNEL_OPAQUE); + // Strip all profiles except color profiles. + foreach ( $this->getImageProfiles( '*', true ) as $key => $value ) { + if ( $key != 'icc' && $key != 'icm' ) { + $this->removeImageProfile( $key ); } + } - // set the image’s bit depth to 8 bits - $this->setImageDepth(8); + if ( method_exists( $this, 'deleteImageProperty' ) ) { + $this->deleteImageProperty( 'comment' ); + $this->deleteImageProperty( 'Thumb::URI' ); + $this->deleteImageProperty( 'Thumb::MTime' ); + $this->deleteImageProperty( 'Thumb::Size' ); + $this->deleteImageProperty( 'Thumb::Mimetype' ); + $this->deleteImageProperty( 'software' ); + $this->deleteImageProperty( 'Thumb::Image::Width' ); + $this->deleteImageProperty( 'Thumb::Image::Height' ); + $this->deleteImageProperty( 'Thumb::Document::Pages' ); + } else { + $this->setImageProperty( 'comment', '' ); + $this->setImageProperty( 'Thumb::URI', '' ); + $this->setImageProperty( 'Thumb::MTime', '' ); + $this->setImageProperty( 'Thumb::Size', '' ); + $this->setImageProperty( 'Thumb::Mimetype', '' ); + $this->setImageProperty( 'software', '' ); + $this->setImageProperty( 'Thumb::Image::Width', '' ); + $this->setImageProperty( 'Thumb::Image::Height', '' ); + $this->setImageProperty( 'Thumb::Document::Pages', '' ); + } - // turn off interlacing - $this->setInterlaceScheme(Imagick::INTERLACE_NO); + // In case user wants to fill use extent for it rather than creating a new canvas + // …fill out the bounding box + if ( $bestfit && $fill && ( $new_width != $columns || $new_height != $rows ) ) { + $extent_x = 0; + $extent_y = 0; - // Strip all profiles except color profiles. - foreach ($this->getImageProfiles('*', true) as $key => $value) { - if ($key != 'icc' && $key != 'icm') { - $this->removeImageProfile($key); - } + if ( $columns > $new_width ) { + $extent_x = ( $columns - $new_width ) / 2; } - if (method_exists($this, 'deleteImageProperty')) { - $this->deleteImageProperty('comment'); - $this->deleteImageProperty('Thumb::URI'); - $this->deleteImageProperty('Thumb::MTime'); - $this->deleteImageProperty('Thumb::Size'); - $this->deleteImageProperty('Thumb::Mimetype'); - $this->deleteImageProperty('software'); - $this->deleteImageProperty('Thumb::Image::Width'); - $this->deleteImageProperty('Thumb::Image::Height'); - $this->deleteImageProperty('Thumb::Document::Pages'); - } else { - $this->setImageProperty('comment', ''); - $this->setImageProperty('Thumb::URI', ''); - $this->setImageProperty('Thumb::MTime', ''); - $this->setImageProperty('Thumb::Size', ''); - $this->setImageProperty('Thumb::Mimetype', ''); - $this->setImageProperty('software', ''); - $this->setImageProperty('Thumb::Image::Width', ''); - $this->setImageProperty('Thumb::Image::Height', ''); - $this->setImageProperty('Thumb::Document::Pages', ''); + if ( $rows > $new_height ) { + $extent_y = ( $rows - $new_height ) / 2; } - // In case user wants to fill use extent for it rather than creating a new canvas - // …fill out the bounding box - if ($bestfit && $fill && ($new_width != $columns || $new_height != $rows)) { - $extent_x = 0; - $extent_y = 0; - - if ($columns > $new_width) { - $extent_x = ($columns - $new_width) / 2; - } - if ($rows > $new_height) { - $extent_y = ($rows - $new_height) / 2; - } - - $this->extentImage($columns, $rows, 0 - $extent_x, $extent_y); - } - - return true; - + $this->extentImage( $columns, $rows, 0 - $extent_x, $extent_y ); } + return true; + } + +} diff --git a/class-wp-image-editor-respimg.php b/class-wp-image-editor-respimg.php index 5683984..123d98e 100644 --- a/class-wp-image-editor-respimg.php +++ b/class-wp-image-editor-respimg.php @@ -1,218 +1,226 @@ file into new Respimg Object. + * + * @access protected * - * @package wp-respimg - * @version 0.0.1 + * @return boolean|WP_Error True if loaded; WP_Error on failure. */ + public function load() { + if ( $this->image instanceof Respimg ) { + return true; + } + + if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) ) { + return new WP_Error( 'error_loading_image', __('File doesn’t exist?'), $this->file ); + } - require_once ABSPATH . WPINC . '/class-wp-image-editor.php'; - require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php'; + /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */ + // Even though Imagick uses less PHP memory than GD, set higher limit for users that have low PHP.ini limits + @ini_set( 'memory_limit', apply_filters( 'image_memory_limit', WP_MAX_MEMORY_LIMIT ) ); + + try { + $this->image = new Respimg( $this->file ); + + if ( ! $this->image->valid() ) { + return new WP_Error( 'invalid_image', __('File is not an image.'), $this->file); + } + // Select the first frame to handle animated images properly + if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) { + $this->image->setIteratorIndex(0); + } + + $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() ); + } catch ( Exception $e ) { + return new WP_Error( 'invalid_image', $e->getMessage(), $this->file ); + } + + $updated_size = $this->update_size(); + if ( is_wp_error( $updated_size ) ) { + return $updated_size; + } + + return $this->set_quality(); + } /** - * WordPress Image Editor Class for Image Manipulation through Imagick PHP Module with php-respimg + * Resizes current image. + * + * At minimum, either a height or width must be provided. + * If one of the two is set to null, the resize will + * maintain aspect ratio according to the provided dimension. + * + * @access public * - * @package wp-respimg - * @uses WP_Image_Editor_Imagick Extends class + * @param int|null $max_w Image width. + * @param int|null $max_h Image height. + * @param boolean $crop + * @return boolean|WP_Error */ - class WP_Image_Editor_Respimg extends WP_Image_Editor_Imagick { - - /** - * Loads image from $this->file into new Respimg Object. - * - * @access protected - * - * @return boolean|WP_Error True if loaded; WP_Error on failure. - */ - public function load() { - if ( $this->image instanceof Respimg ) - return true; + public function resize( $max_w, $max_h, $crop = false ) { + if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) ) { + return true; + } - if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) ) - return new WP_Error( 'error_loading_image', __('File doesn’t exist?'), $this->file ); + $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop ); + if ( ! $dims ) { + return new WP_Error( 'error_getting_dimensions', __('Could not calculate resized image dimensions') ); + } - /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */ - // Even though Imagick uses less PHP memory than GD, set higher limit for users that have low PHP.ini limits - @ini_set( 'memory_limit', apply_filters( 'image_memory_limit', WP_MAX_MEMORY_LIMIT ) ); + list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims; - try { - $this->image = new Respimg( $this->file ); + if ( $crop ) { + return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h ); + } - if( ! $this->image->valid() ) - return new WP_Error( 'invalid_image', __('File is not an image.'), $this->file); + try { + if ($this->mime_type === 'image/gif') { + $this->image->scaleImage( $dst_w, $dst_h ); + } else { + $this->image->smartResize( $dst_w, $dst_h, false ); + } + } + catch ( Exception $e ) { + return new WP_Error( 'image_resize_error', $e->getMessage() ); + } - // Select the first frame to handle animated images properly - if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) - $this->image->setIteratorIndex(0); + return $this->update_size( $dst_w, $dst_h ); + } - $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() ); - } - catch ( Exception $e ) { - return new WP_Error( 'invalid_image', $e->getMessage(), $this->file ); + /** + * Resize multiple images from a single source. + * + * @access public + * + * @param array $sizes { + * An array of image size arrays. Default sizes are 'small', 'medium', 'large'. + * + * Either a height or width must be provided. + * If one of the two is set to null, the resize will + * maintain aspect ratio according to the provided dimension. + * + * @type array $size { + * @type int ['width'] Optional. Image width. + * @type int ['height'] Optional. Image height. + * @type bool $crop Optional. Whether to crop the image. Default false. + * } + * } + * @return array An array of resized images' metadata by size. + */ + public function multi_resize( $sizes ) { + $metadata = array(); + $orig_size = $this->size; + $orig_image = clone $this->image; + + foreach ( $sizes as $size => $size_data ) { + if ( ! $this->image ) { + $this->image = clone $orig_image; } - $updated_size = $this->update_size(); - if ( is_wp_error( $updated_size ) ) { - return $updated_size; + if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { + continue; } - return $this->set_quality(); - } - - /** - * Resizes current image. - * - * At minimum, either a height or width must be provided. - * If one of the two is set to null, the resize will - * maintain aspect ratio according to the provided dimension. - * - * @access public - * - * @param int|null $max_w Image width. - * @param int|null $max_h Image height. - * @param boolean $crop - * @return boolean|WP_Error - */ - public function resize( $max_w, $max_h, $crop = false ) { - if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) ) - return true; - - $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop ); - if ( ! $dims ) - return new WP_Error( 'error_getting_dimensions', __('Could not calculate resized image dimensions') ); - list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims; - - if ( $crop ) { - return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h ); + if ( ! isset( $size_data['width'] ) ) { + $size_data['width'] = null; } - - try { - if ($this->mime_type === 'image/gif') { - $this->image->scaleImage( $dst_w, $dst_h ); - } else { - $this->image->smartResize( $dst_w, $dst_h, false ); - } + if ( ! isset( $size_data['height'] ) ) { + $size_data['height'] = null; } - catch ( Exception $e ) { - return new WP_Error( 'image_resize_error', $e->getMessage() ); + + if ( ! isset( $size_data['crop'] ) ) { + $size_data['crop'] = false; } - return $this->update_size( $dst_w, $dst_h ); - } + $resize_result = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] ); + $duplicate = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) ); - /** - * Resize multiple images from a single source. - * - * @access public - * - * @param array $sizes { - * An array of image size arrays. Default sizes are 'small', 'medium', 'large'. - * - * Either a height or width must be provided. - * If one of the two is set to null, the resize will - * maintain aspect ratio according to the provided dimension. - * - * @type array $size { - * @type int ['width'] Optional. Image width. - * @type int ['height'] Optional. Image height. - * @type bool $crop Optional. Whether to crop the image. Default false. - * } - * } - * @return array An array of resized images' metadata by size. - */ - public function multi_resize( $sizes ) { - $metadata = array(); - $orig_size = $this->size; - $orig_image = clone $this->image; - - foreach ( $sizes as $size => $size_data ) { - if ( ! $this->image ) - $this->image = clone $orig_image; - - if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { - continue; - } + if ( ! is_wp_error( $resize_result ) && ! $duplicate ) { + $resized = $this->_save( $this->image ); - if ( ! isset( $size_data['width'] ) ) { - $size_data['width'] = null; - } - if ( ! isset( $size_data['height'] ) ) { - $size_data['height'] = null; - } + $this->image->clear(); + $this->image->destroy(); + $this->image = null; - if ( ! isset( $size_data['crop'] ) ) { - $size_data['crop'] = false; + if ( ! is_wp_error( $resized ) && $resized ) { + unset( $resized['path'] ); + $metadata[$size] = $resized; } + } - $resize_result = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] ); - $duplicate = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) ); - - if ( ! is_wp_error( $resize_result ) && ! $duplicate ) { - $resized = $this->_save( $this->image ); + $this->size = $orig_size; + } - $this->image->clear(); - $this->image->destroy(); - $this->image = null; + $this->image = $orig_image; - if ( ! is_wp_error( $resized ) && $resized ) { - unset( $resized['path'] ); - $metadata[$size] = $resized; - } - } + return $metadata; + } - $this->size = $orig_size; - } + /** + * Crops Image. + * + * @access public + * + * @param int $src_x The start x position to crop from. + * @param int $src_y The start y position to crop from. + * @param int $src_w The width to crop. + * @param int $src_h The height to crop. + * @param int $dst_w Optional. The destination width. + * @param int $dst_h Optional. The destination height. + * @param boolean $src_abs Optional. If the source crop points are absolute. + * @return boolean|WP_Error + */ + public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { + if ( $src_abs ) { + $src_w -= $src_x; + $src_h -= $src_y; + } - $this->image = $orig_image; + try { + $this->image->cropImage( $src_w, $src_h, $src_x, $src_y ); + $this->image->setImagePage( $src_w, $src_h, 0, 0); - return $metadata; - } + if ( $dst_w || $dst_h ) { + // If destination width/height isn't specified, use same as + // width/height from source. + if ( ! $dst_w ) { + $dst_w = $src_w; + } - /** - * Crops Image. - * - * @access public - * - * @param int $src_x The start x position to crop from. - * @param int $src_y The start y position to crop from. - * @param int $src_w The width to crop. - * @param int $src_h The height to crop. - * @param int $dst_w Optional. The destination width. - * @param int $dst_h Optional. The destination height. - * @param boolean $src_abs Optional. If the source crop points are absolute. - * @return boolean|WP_Error - */ - public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { - if ( $src_abs ) { - $src_w -= $src_x; - $src_h -= $src_y; - } + if ( ! $dst_h ) { + $dst_h = $src_h; + } - try { - $this->image->cropImage( $src_w, $src_h, $src_x, $src_y ); - $this->image->setImagePage( $src_w, $src_h, 0, 0); - - if ( $dst_w || $dst_h ) { - // If destination width/height isn't specified, use same as - // width/height from source. - if ( ! $dst_w ) - $dst_w = $src_w; - if ( ! $dst_h ) - $dst_h = $src_h; - - if ($this->mime_type === 'image/gif') { - $this->image->scaleImage( $dst_w, $dst_h ); - } else { - $this->image->smartResize( $dst_w, $dst_h, false ); - } - return $this->update_size(); + if ($this->mime_type === 'image/gif') { + $this->image->scaleImage( $dst_w, $dst_h ); + } else { + $this->image->smartResize( $dst_w, $dst_h, false ); } + return $this->update_size(); } - catch ( Exception $e ) { - return new WP_Error( 'image_crop_error', $e->getMessage() ); - } - return $this->update_size(); + } catch ( Exception $e ) { + return new WP_Error( 'image_crop_error', $e->getMessage() ); } - + return $this->update_size(); } + +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..dff1034 --- /dev/null +++ b/composer.json @@ -0,0 +1,10 @@ +{ + "name" : "ResponsiveImagesCG/wp-tevko-responsive-images", + "description": "Fully responsive image plugin for WordPress.", + "homepage" : "https://github.com/ResponsiveImagesCG/wp-tevko-responsive-images", + "type" : "wordpress-plugin", + "license" : "GPL-2.0+", + "require" : { + "composer/installers": "~1.0" + } +} diff --git a/readme.md b/readme.md index eaca33c..0c71e8e 100644 --- a/readme.md +++ b/readme.md @@ -7,34 +7,39 @@ Bringing automatic default responsive images to WordPress. This plugin works by including all available image sizes for each image upload. Whenever WordPress outputs the image through the media uploader, or whenever a featured image is generated, those sizes will be included in the image tag via the [srcset](http://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-srcset/) attribute. -##Contribution Guidelines +## Contribution Guidelines Please submit pull requests to our dev branch. If your contribution requires such, please aim to include appropriate tests with your pr as well. -##Documentation +## Documentation -###For General Users +### For General Users No configuration is needed! Just install the plugin and enjoy automatic responsive images! -###For Theme Developers +### For Theme Developers This plugin includes several functions that can be used by theme and plugin developers in templates. -###Advanced Image Compression +### Advanced Image Compression -**This is an experimental feature, if used, please provide us with feedback!** +Advanced image compression is an experimental image editor that makes use of ImageMagick's compression setting to deliver deliver higher quality images at a smaller file sizes. As such, **ImageMagick is required for this feature to work**. To learn more about the actual compression settings being used, read Dave Newton's [excellent writeup at Smashing Magazine](http://www.smashingmagazine.com/2015/06/efficient-image-resizing-with-imagemagick/). -This feature turns on advanced compression, which will deliver higher quality images at a smaller file size. To enable, place the following code in your `functions.php` file - +To enable, place the following code in your `functions.php` file - ``` function custom_theme_setup() { add_theme_support( 'advanced-image-compression' ); } add_action( 'after_setup_theme', 'custom_theme_setup' ); ``` + +***Known issues:*** +* Some people have encountered memory limits when uploading large files with the advanced image compression settings enabled (see [#150](https://github.com/ResponsiveImagesCG/wp-tevko-responsive-images/issues/150)). + + --- -####tevkori_get_sizes( $id, $size, $args ) +#### tevkori_get_sizes( $id, $size, $args ) Returns a valid source size value for use in a 'sizes' attribute. The parameters include the ID of the image, the default size of the image, and an array or string containing of size information. The ID parameter is required. [Link](https://github.com/ResponsiveImagesCG/wp-tevko-responsive-images/blob/master/wp-tevko-responsive-images.php#L28) @@ -79,7 +84,7 @@ Which would output a sizes value of: --- -####tevkori_get_sizes_string( $id, $size, $args) +#### tevkori_get_sizes_string( $id, $size, $args) Returns A full 'sizes' attribute. The parameters include the ID of the image, the default size of the image, and an array or string containing of size information. The ID parameter is required. @@ -90,7 +95,7 @@ Returns A full 'sizes' attribute. The parameters include the ID of the image, th ``` --- -####tevkori_get_srcset_array( $id, $size ) +#### tevkori_get_srcset_array( $id, $size ) Returns an array of image source candidates for use in a 'srcset' attribute. The parameters include the ID of the image, the default size of the image, and An array of of srcset values. The ID parameter is required. [Link](https://github.com/ResponsiveImagesCG/wp-tevko-responsive-images/blob/master/wp-tevko-responsive-images.php#L132) @@ -111,7 +116,7 @@ foreach( $sources as $key => $source ) { --- -####tevkori_get_srcset_string( $id, $size ) +#### tevkori_get_srcset_string( $id, $size ) Returns A full 'srcset' attribute. The parameters include the ID of the image and its default size. The ID parameter is required. [Link](https://github.com/ResponsiveImagesCG/wp-tevko-responsive-images/blob/master/wp-tevko-responsive-images.php#L196) @@ -133,11 +138,29 @@ The only external dependency included in this plugin is [Picturefill](http://sco We use a hook because if you attempt to dequeue a script before it's enqueued, wp_dequeue_script has no effect. (If it's still being loaded, you may need to specify a [priority](http://codex.wordpress.org/Function_Reference/add_action).) -##Version - -2.3.1 - -##Changelog +## Version +2.4.0 + +## Changelog + +- Added filter for tevkori_get_sizes, with tests +- Added Composer support +- Compare aspect ratio in relative values, not absolute values +- Cleanup of code style and comments added +- Added PHP 5.2 to our Travis test matrix +- Fixed unit test loading +- Preventing duplicates in srcset array +- Updated docs for advanced image compression +- Formatting cleanup in readme.md +- Bump plugin 'Tested up to:' value to 4.3 +- Remove extra line from readme.txt +- Added changelog items from 2.3.1 to the readme.txt file +- Added 'sudo: false' to travis.ci to use new TravisCI infrastructure +- Removing the srcset and sizes attributes if there is only one source present for the image +- Use edited image hash to filter out originals from edited images +- Make output of tevkori_get_srcset_array filterable + +**2.3.1** - First char no longer stripped from file name if there's no slash - Adding test for when uploads directory not organized by date diff --git a/readme.txt b/readme.txt index 92bb5d3..a238163 100644 --- a/readme.txt +++ b/readme.txt @@ -1,10 +1,10 @@ === RICG Responsive Images === -Contributors: tevko, wilto, chriscoyier, joemcgill, Michael McGinnis, ryelle, drrobotnik, nacin , georgestephanis, helen, wordpressdotorg, Bocoup +Contributors: tevko, wilto, joemcgill, jaspermdegroot, chriscoyier, Michael McGinnis, ryelle, drrobotnik, nacin , georgestephanis, helen, wordpressdotorg, Bocoup Donate link: https://app.etapestry.com/hosted/BoweryResidentsCommittee/OnlineDonation.html Tags: Responsive, Images, Responsive Images, SRCSET, Picturefill Requires at least: 4.1 -Tested up to: 4.2.2 -Stable tag: 2.3.1 +Tested up to: 4.3 +Stable tag: 2.4.0 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.txt @@ -25,6 +25,25 @@ This plugin works by including all available image sizes for each image upload. 3. If you'd like to enable the advanced image compression feature, Please see the instructions at https://github.com/ResponsiveImagesCG/wp-tevko-responsive-images/tree/dev#advanced-image-compression == Changelog == + += 2.4.0 = +* Added filter for tevkori_get_sizes, with tests +* Added Composer support +* Compare aspect ratio in relative values, not absolute values +* Cleanup of code style and comments added +* Added PHP 5.2 to our Travis test matrix +* Fixed unit test loading +* Preventing duplicates in srcset array +* Updated docs for advanced image compression +* Formatting cleanup in readme.md +* Bump plugin 'Tested up to:' value to 4.3 +* Remove extra line from readme.txt +* Added changelog items from 2.3.1 to the readme.txt file +* Added 'sudo: false' to travis.ci to use new TravisCI infrastructure +* Removing the srcset and sizes attributes if there is only one source present for the image +* Use edited image hash to filter out originals from edited images +* Make output of tevkori_get_srcset_array filterable + = 2.3.1 = * First char no longer stripped from file name if there's no slash * Adding test for when uploads directory not organized by date @@ -52,26 +71,25 @@ This plugin works by including all available image sizes for each image upload. * Check if wp.media exists before running JS * Account for rounding variance when matching ascpect ratios - = 2.1.1 = * Adding in wp-tevko-responsive-images.js after file not found to be in WordPress repository * Adjusts the aspect ratio check in tevkori_get_srcset_array() to account for rounding variance = 2.1.0 = - * **This version introduces a breaking change** - there are now two functions. One returns an array of srcset values, and the other returns a string with the `srcset=".."` html needed to generate the responsive image. To retrieve the srcset array, us `tevkori_get_srcset_array( $id, $size )` - * When the image size is changed in the post editor, the srcset values will adjust to match the change. +* **This version introduces a breaking change** - there are now two functions. One returns an array of srcset values, and the other returns a string with the `srcset=".."` html needed to generate the responsive image. To retrieve the srcset array, us `tevkori_get_srcset_array( $id, $size )` +* When the image size is changed in the post editor, the srcset values will adjust to match the change. = 2.0.2 = - * A bugfix correcting a divide by zero error. Some users may have seen this after upgrading to 2.0.1 +* A bugfix correcting a divide by zero error. Some users may have seen this after upgrading to 2.0.1 = 2.0.1 = - * Only outputs the default WordPress sizes, giving theme developers the option to extend as needed - * Added support for featured images +* Only outputs the default WordPress sizes, giving theme developers the option to extend as needed +* Added support for featured images = 2.0.0 = - * Uses [Picturefill 2.2.0 (Beta)](http://scottjehl.github.io/picturefill/) - * Scripts are output to footer - * Image sizes adjusted - * Most importantly, the srcset syntax is being used - - Works for cropped images! - - Backwards compatible (images added before plugin install will still be responsive)! +* Uses [Picturefill 2.2.0 (Beta)](http://scottjehl.github.io/picturefill/) +* Scripts are output to footer +* Image sizes adjusted +* Most importantly, the srcset syntax is being used +* Works for cropped images! +* Backwards compatible (images added before plugin install will still be responsive)! diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3be4181..58e9fce 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,13 +1,34 @@ assertSame($expected, $sizes); } + function test_filter_tevkori_get_sizes_string() { + // Add our test filter. + add_filter( 'tevkori_image_sizes_args', array( $this, '_test_tevkori_image_sizes_args' ) ); + + // Set up our test. + $id = $this->_test_img(); + $sizes = tevkori_get_sizes($id, 'medium'); + + // Evaluate that the sizes returned is what we expected. + $this->assertSame( $sizes, '100vm'); + + remove_filter( 'tevkori_image_sizes_args', array( $this, '_test_tevkori_image_sizes_args' ) ); + } + + /** + * A simple test filter for tevkori_get_sizes(). + */ + function _test_tevkori_image_sizes_args( $args ) { + $args['sizes'] = "100vm"; + return $args; + } + + function test_filter_tevkori_srcset_array() { + // Add test filter + add_filter( 'tevkori_srcset_array', array( $this, '_test_tevkori_srcset_array' ) ); + + // Set up our test. + $id = $this->_test_img(); + $sizes = tevkori_get_srcset_array($id, 'medium'); + + // Evaluate that the sizes returned is what we expected. + foreach( $sizes as $width => $source ) { + $this->assertTrue( $width <= 500 ); + } + + // Remove test filter + remove_filter( 'tevkori_srcset_array', array( $this, '_test_tevkori_srcset_array' ) ); + } + + /** + * A test filter for tevkori_get_srcset_array() that removes any sources + * that are larger that 500px wide. + */ + function _test_tevkori_srcset_array( $array ) { + foreach ( $array as $size => $file ) { + if ( $size > 500 ) { + unset( $array[$size] ); + } + } + return $array; + } + function test_tevkori_get_sizes_string() { // make an image $id = $this->_test_img(); @@ -134,14 +186,13 @@ function test_tevkori_get_srcset_array() { $year_month = date('Y/m'); $image = wp_get_attachment_metadata( $id ); - $filename_base = substr( $image['file'], 0, strrpos($image['file'], '.png') ); $expected = array( - 'http://example.org/wp-content/uploads/' . $year_month = date('Y/m') . '/' + $image['sizes']['medium']['width'] => 'http://example.org/wp-content/uploads/' . $year_month = date('Y/m') . '/' . $image['sizes']['medium']['file'] . ' ' . $image['sizes']['medium']['width'] . 'w', - 'http://example.org/wp-content/uploads/' . $year_month = date('Y/m') . '/' + $image['sizes']['large']['width'] => 'http://example.org/wp-content/uploads/' . $year_month = date('Y/m') . '/' . $image['sizes']['large']['file'] . ' ' . $image['sizes']['large']['width'] . 'w', - 'http://example.org/wp-content/uploads/' . $image['file'] . ' ' . $image['width'] .'w' + $image['width'] => 'http://example.org/wp-content/uploads/' . $image['file'] . ' ' . $image['width'] .'w' ); $this->assertSame( $expected, $sizes ); @@ -159,12 +210,11 @@ function test_tevkori_get_srcset_array_no_date_upoads() { $sizes = tevkori_get_srcset_array( $id, 'medium' ); $image = wp_get_attachment_metadata( $id ); - $filename_base = substr( $image['file'], 0, strrpos($image['file'], '.png') ); $expected = array( - 'http://example.org/wp-content/uploads/' . $image['sizes']['medium']['file'] . ' ' . $image['sizes']['medium']['width'] . 'w', - 'http://example.org/wp-content/uploads/' . $image['sizes']['large']['file'] . ' ' . $image['sizes']['large']['width'] . 'w', - 'http://example.org/wp-content/uploads/' . $image['file'] . ' ' . $image['width'] .'w' + $image['sizes']['medium']['width'] => 'http://example.org/wp-content/uploads/' . $image['sizes']['medium']['file'] . ' ' . $image['sizes']['medium']['width'] . 'w', + $image['sizes']['large']['width'] => 'http://example.org/wp-content/uploads/' . $image['sizes']['large']['file'] . ' ' . $image['sizes']['large']['width'] . 'w', + $image['width'] => 'http://example.org/wp-content/uploads/' . $image['file'] . ' ' . $image['width'] .'w' ); $this->assertSame( $expected, $sizes ); @@ -173,23 +223,53 @@ function test_tevkori_get_srcset_array_no_date_upoads() { update_option( 'uploads_use_yearmonth_folders', $uploads_use_yearmonth_folders ); } - function test_tevkori_get_srcset_array_thumb() { + function test_tevkori_get_srcset_array_single_srcset() { // make an image $id = $this->_test_img(); + // In our tests, thumbnails would only return a single srcset candidate, + // in which case we don't bother returning a srcset array. $sizes = tevkori_get_srcset_array( $id, 'thumbnail' ); - $image = wp_get_attachment_metadata( $id ); + $this->assertFalse( $sizes ); + } - $year_month = date('Y/m'); - $expected = array( - 'http://example.org/wp-content/uploads/' . $year_month = date('Y/m') . '/' - . $image['sizes']['thumbnail']['file'] . ' ' . $image['sizes']['thumbnail']['width'] . 'w', - ); + /** + * Test for filtering out leftover sizes after an image is edited. + * @group 155 + */ + function test_tevkori_get_srcset_array_with_edits() { + // Make an image. + $id = $this->_test_img(); - $this->assertSame( $expected, $sizes ); + // For this test we're going to mock metadata changes from an edit. + // Start by getting the attachment metadata. + $meta = wp_get_attachment_metadata( $id ); + + // Mimick hash generation method used in wp_save_image(). + $hash = 'e' . time() . rand(100, 999); + + // Replace file paths for full and medium sizes with hashed versions. + $filename_base = basename( $meta['file'], '.png' ); + $meta['file'] = str_replace( $filename_base, $filename_base . '-' . $hash, $meta['file'] ); + $meta['sizes']['medium']['file'] = str_replace( $filename_base, $filename_base . '-' . $hash, $meta['sizes']['medium']['file'] ); + + // Save edited metadata. + wp_update_attachment_metadata( $id, $meta ); + + // Get the edited image and observe that a hash was created. + $img_url = wp_get_attachment_url( $id ); + + // Calculate a srcset array. + $sizes = tevkori_get_srcset_array( $id, 'medium' ); + + // Test to confirm all sources in the array include the same edit hash. + foreach ( $sizes as $size ) { + $this->assertTrue( false !== strpos( $size, $hash ) ); + } } - function test_tevkori_get_srcset_array_false() { // make an image + function test_tevkori_get_srcset_array_false() { + // make an image $id = $this->_test_img(); $sizes = tevkori_get_srcset_array( 99999, 'foo' ); diff --git a/wp-tevko-responsive-images.php b/wp-tevko-responsive-images.php index 550b838..46991cb 100644 --- a/wp-tevko-responsive-images.php +++ b/wp-tevko-responsive-images.php @@ -1,6 +1,4 @@ $img_meta['width'], 'height' => $img_meta['height'], - 'file' => $img_meta['file'] + 'file' => $img_meta['file'] ); + if ( strrpos( $img_meta['file'], '/' ) !== false ) { $img_sizes['full']['file'] = substr( $img_meta['file'], strrpos( $img_meta['file'], '/' ) + 1 ); } @@ -180,23 +209,43 @@ function tevkori_get_srcset_array( $id, $size = 'thumbnail' ) { // Calculate the image aspect ratio. $img_ratio = $img_height / $img_width; - // Only use sizes with same aspect ratio. - foreach ( $img_sizes as $img_size => $img ) { + // Images that have been edited in WordPres after being uploaded will + // contain a unique hash. We look for that hash and use it later to filter + // out images that are left overs from previous renditions. + $img_edited = preg_match( '/-e[0-9]{13}/', $img_url, $img_edit_hash ); - // Calculate the height we would expect if the image size has the same aspect ratio. - $expected_height = (int) round( $img['width'] * $img_ratio ); + // Loop through available images and only use images that are resized + // versions of the same rendition. + foreach ( $img_sizes as $img ) { - // If image height doesn't varies more than 2px over the expected, use it. - if ( $img['height'] >= $expected_height - 2 && $img['height'] <= $expected_height + 2 ) { - $arr[] = $img_base_url . $img['file'] . ' ' . $img['width'] .'w'; + // Filter out images that are leftovers from previous renditions. + if ( $img_edited && ! strpos( $img['file'], $img_edit_hash[0] ) ) { + continue; + } + + // Calculate the new image ratio. + $img_ratio_compare = $img['height'] / $img['width']; + + // If the new ratio differs by less than 0.01, use it. + if ( abs( $img_ratio - $img_ratio_compare ) < 0.01 ) { + $arr[ $img['width'] ] = $img_base_url . $img['file'] . ' ' . $img['width'] .'w'; } } - if ( empty( $arr ) ) { + if ( count( $arr ) <= 1 ) { return false; } - return $arr; + /** + * Filter the output of tevkori_get_srcset_array(). + * + * @since 2.4.0 + * + * @param array $arr An array of image sources. + * @param int $id Attachment ID for image. + * @param array|string $size Size of image, either array or string. + */ + return apply_filters( 'tevkori_srcset_array', $arr, $id, $size ); } /** @@ -260,19 +309,19 @@ function tevkori_get_src_sizes( $id, $size = 'thumbnail' ) { function tevkori_extend_image_tag( $html, $id, $caption, $title, $align, $url, $size, $alt ) { add_filter( 'editor_max_image_size', 'tevkori_editor_image_size' ); - $sizes = tevkori_get_sizes( $id, $size ); - - // Build the data-sizes attribute if sizes were returned. - if ( $sizes ) { - $sizes = 'data-sizes="' . $sizes . '"'; - } - - // Build the srcset attribute. + // Get the srcset attribute. $srcset = tevkori_get_srcset_string( $id, $size ); remove_filter( 'editor_max_image_size', 'tevkori_editor_image_size' ); - $html = preg_replace( '/(src\s*=\s*"(.+?)")/', '$1 ' . $sizes . ' ' . $srcset, $html ); + if ( $srcset ) { + + // Build the data-sizes attribute if sizes were returned. + $sizes = tevkori_get_sizes( $id, $size ); + $sizes = $sizes ? 'data-sizes="' . $sizes . '"' : ''; + + $html = preg_replace( '/(src\s*=\s*"(.+?)")/', '$1 ' . $sizes . ' ' . $srcset, $html ); + } return $html; } @@ -285,20 +334,22 @@ function tevkori_extend_image_tag( $html, $id, $caption, $title, $align, $url, $ * @return array Attributes for image. */ function tevkori_filter_attachment_image_attributes( $attr, $attachment, $size ) { - $attachment_id = $attachment->ID; + if ( ! isset( $attr['srcset'] ) ) { + $srcset = tevkori_get_srcset( $attachment->ID, $size ); - if ( ! isset( $attr['sizes'] ) ) { - $sizes = tevkori_get_sizes( $attachment_id, $size ); + // Set the srcset attribute if one was returned. + if ( $srcset ) { + $attr['srcset'] = $srcset; - // Build the sizes attribute if sizes were returned. - if ( $sizes ) { - $attr['sizes'] = $sizes; - } - } + if ( ! isset( $attr['sizes'] ) ) { + $sizes = tevkori_get_sizes( $attachment->ID, $size ); - if ( ! isset( $attr['srcset'] ) ) { - $srcset = tevkori_get_srcset( $attachment_id, $size ); - $attr['srcset'] = $srcset; + // Set the sizes attribute if sizes were returned. + if ( $sizes ) { + $attr['sizes'] = $sizes; + } + } + } } return $attr; @@ -308,10 +359,9 @@ function tevkori_filter_attachment_image_attributes( $attr, $attachment, $size ) /** * Disable the editor size constraint applied for images in TinyMCE. * - * @param array $max_image_size An array with the width as the first element, and the height as the second element. * @return array A width & height array so large it shouldn't constrain reasonable images. */ -function tevkori_editor_image_size( $max_image_size ) { +function tevkori_editor_image_size() { return array( 99999, 99999 ); } @@ -338,29 +388,12 @@ function tevkori_load_admin_scripts( $hook ) { function tevkori_filter_content_sizes( $content ) { $images = '/(