<?php
/**
 * Filter the output of image_downsize() to return dynamically generated images for intermediate or inline sizes.
 *
 * <p>Because Wordpress generates all image sizes on first upload, if you change
 * theme or size settings after the upload, there won't be a matching file for
 * the requested size.<br/>
 * This filter addresses the problem of the default downsize process laoding
 * a large file and scaling it down in the browser if it doesn't find the right
 * size image. This can cause large files to be loaded unnecessarily and will
 * only scale it to a max width or height using the proportions of the next best
 * image, it won't crop to the exact dimensions.</p>
 * <p>Other solutions involve either patching Wordpress, using custom functions or
 * using plugins that regenerate images manually. They work with some success but
 * don't satisfy all the following ideal (imo) criteria:</p>
 * <ul>
 *    <li>Regenerate images automatically when required by the current theme</li>
 *    <li>Don't require a plugin to manage media and extra images sizes</li>
 *    <li>Don't use depreciated or custom image processing functions</li>
 *    <li>Let wordpress handle creation, paths, error checking, library detection</li>
 *    <li>Allow other filters to still apply (don't bypass filtered functions)</li>
 *    <li>Keep generated sizes in meta to avoid orphan files in uploads folder</li>
 *    <li>Can be disabled/removed without errors or changing anything else</li>
 * </ul>
 * <p>This does all that! :D</p>
 * <p>The issue was well defined in 2010 by Victor Teixeira and subsiquent
 * contributor repos, more recently by Eric Lewis. See links.</p>
 * <p>Example usage with theme specific size:
 * <br/>In theme functions.php: `add_image_size( 'Feature Image', 565, 337, true );`
 * <br/>In template file: `the_post_thumbnail( 'Feature Image' );`</p>
 * <p>Example usage with on-the-fly dimensions:<br/>
 * <br/>In template file: `the_post_thumbnail( array( 565, 337 ) );`
 * <br/>Will create a new named size called '565x337'</p>
 * <p>Returning a truthy value to the filter will effectively short-circuit
 * down-sizing the image, returning that value as output instead.</p>
 *
 * @param bool         $downsize Whether to short-circuit the image downsize. Default passed in true. (ignored in filter)
 * @param int          $id       Attachment ID for image.
 * @param array|string $size     Size of image, either array or string. Default passed in 'medium'.
 *
 * @return array|bool            [ Image URL, width, height, bool ($is_intermediate) ] OR false if not resizing
 * @author Tim Kinnane <tim@nestedcode.com>
 * @link   http://nestedcode.com
 * @link   https://core.trac.wordpress.org/ticket/15311
 * @link   https://gist.github.com/seedprod/1367237
 * @link   https://core.trac.wordpress.org/attachment/ticket/15311/15311.7.diff*
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Direct access not allowed.
}

// Do not execute on admin side
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
	return;
}

// Dynamic Image Downsize
function dynamic_image_downsize( $downsize = true, $id, $size, $crop = true ) {
	$meta  = wp_get_attachment_metadata( $id );
	$sizes = get_image_sizes();

	// use specific w/h dimensions requested
	if ( is_array( $size ) && sizeof( $size ) == 2 ) {
		list( $width, $height ) = $size;

		// make a size name from requested dimensions as a fallback for saving to meta
		$size = $width . 'x' . $height;

		// if dimensions match a named size, use that instead
		foreach ( $sizes as $size_name => $size_atts ) {
			if ( $width == $size_atts['width'] && $height == $size_atts['height'] ) {
				$size = $size_name;
			}
		}

		/* If $size is not a custom array, exit to handle as normal. This breaks the dynamic resizing for named image sizes
		   (i.e. Regenerate Thumbnail functionality) but greatly improves performance for custom image sizes. */
	} else {
		return false;
	}

	// exit if there's already a generated image with the right dimensions
	// the default downsize function would use it anyway (even if it had a different name)
	if ( isset( $meta['sizes'] ) && array_key_exists( $size, $meta['sizes'] ) && ( $width == $meta['sizes'][ $size ]['width'] || $height == $meta['sizes'][ $size ]['height'] ) ) {
		return false;
	}

	// nothing matching size exists, generate and save new image from original
	$intermediate = image_make_intermediate_size( get_attached_file( $id ), $width, $height, $crop );

	// exit if failed creating image
	if ( ! is_array( $intermediate ) ) {
		return false;
	}

	// save the new size parameters in meta (to find it next time)
	$meta['sizes'][ $size ] = $intermediate;
	wp_update_attachment_metadata( $id, $meta );

	// this step is from the default image_downsize function in media.php
	// "might need to further constrain it if content_width is narrower"
	list( $width, $height ) = image_constrain_size_for_editor( $intermediate['width'], $intermediate['height'], $size );

	// use path of original file with new filename
	$original_url      = wp_get_attachment_url( $id );
	$original_basename = wp_basename( $original_url );
	$img_url           = str_replace( $original_basename, $intermediate['file'], $original_url );

	// 'Tis done, and here's the image
	return [ $img_url, $width, $height, true ];
}

add_filter( 'image_downsize', 'dynamic_image_downsize', 10, 3 );

// Useful helper function from codex example: http://codex.wordpress.org/Function_Reference/get_intermediate_image_sizes
function get_image_sizes( $size = '' ) {
	global $_wp_additional_image_sizes;
	$sizes                        = [];
	$get_intermediate_image_sizes = get_intermediate_image_sizes();

	// Create the full array with sizes and crop info
	foreach ( $get_intermediate_image_sizes as $_size ) {
		if ( in_array( $_size, [ 'thumbnail', 'medium', 'large' ] ) ) {
			$sizes[ $_size ]['width']  = get_option( $_size . '_size_w' );
			$sizes[ $_size ]['height'] = get_option( $_size . '_size_h' );
			$sizes[ $_size ]['crop']   = (bool) get_option( $_size . '_crop' );
		} elseif ( isset( $_wp_additional_image_sizes[ $_size ] ) ) {
			$sizes[ $_size ] = [
				'width'  => $_wp_additional_image_sizes[ $_size ]['width'],
				'height' => $_wp_additional_image_sizes[ $_size ]['height'],
				'crop'   => $_wp_additional_image_sizes[ $_size ]['crop']
			];
		}
	}

	// Get only 1 size if found
	if ( $size ) {
		if ( isset( $sizes[ $size ] ) ) {
			return $sizes[ $size ];
		} else {
			return false;
		}
	}

	return $sizes;
}
