* @copyright Copyright (c) 2018, Pixelative
* @since 1.0.0
*/
class Amp_WP_IMG_Component extends Amp_WP_Component_Base implements Amp_WP_Component_Interface {
/**
* Constructor.
*
* @since 1.0.0
*/
public function head() {
if( !amp_wp_is_customize_preview()) {
add_filter('post_thumbnail_html', array($this, 'transform_image_tag_to_amp'));
add_filter('get_avatar', array($this, 'transform_image_tag_to_amp'));
}
}
/**
* Contract implementation
*
* @since 1.0.0
*
* @return array
*/
public function config() {
return array(
'scripts' => array(
'amp-anim' => 'https://cdn.ampproject.org/v0/amp-anim-0.1.js'
)
);
}
/**
* Transform
tags to the or tags
*
* @param Amp_WP_Html_Util $instance
*
* @return Amp_WP_Html_Util
* @since 1.0.0
*
*/
public function transform(Amp_WP_Html_Util $instance) {
$elements = $instance->getElementsByTagName('img'); // get all img tags
/**
* @var DOMElement $element
*/
if ($nodes_count = $elements->length) {
for ($i = $nodes_count - 1; $i >= 0; $i --) {
$element = $elements->item($i);
if ($this->is_animated_image_element($element)) {
$this->enable_enqueue_scripts = true;
$tag_name = 'amp-anim';
} else {
$tag_name = 'amp-img';
}
$attributes = $instance->filter_attributes($instance->get_node_attributes($element));
$attributes = $this->modify_attributes($attributes);
$attributes = $this->filter_attributes($attributes, $tag_name);
$instance->replace_node($element, $tag_name, $attributes);
}
}
return $instance;
}
/**
* Append or modify amp-img|amp-anim attributes
*
* @param array $attributes
*
* @since 1.0.0
*
* @return array
*/
protected function modify_attributes($attributes) {
if (!isset($attributes['class'])) {
$attributes['class'] = '';
}
$attributes['class'] .= ' amp-image-tag';
if (!isset($attributes['width']) || !isset($attributes['height'])) {
if (isset($attributes['src'])) {
if ($dim = $this->get_image_dimension($attributes['src'])) {
$attributes['width'] = $dim[0];
$attributes['height'] = $dim[1];
}
}
}
return $this->enforce_sizes_attribute($attributes);
}
/**
* Filter amp-img | amp-anim attributes list
*
* todo list all valid amp attributes for amp-img & amp-anim
*
* @param array $attributes
* @param string $tag_name
*
* @since 1.0.0
*
* @return array
*/
protected function filter_attributes($attributes, $tag_name) {
$valid_atts = array(
'src',
'height',
'width',
'class',
'alt',
'sizes',
'on'
);
return amp_wp_filter_attributes($attributes, $valid_atts, $tag_name);
}
/**
* Detect is given
element is animation
*
* @param DOMElement $element
element object
*
* @since 1.0.0
*
* @return bool true if image was animated or false otherwise
*/
protected function is_animated_image_element($element) {
$src = $element->attributes->getNamedItem('src'); // get src attribute
if ($src && isset($src->value)) {
return $this->is_animated_image_url($src->value);
}
$class = $element->attributes->getNamedItem('class');
if ($class && isset($class->value)) {
return preg_match('/\b animated-img \b/ix', $class->value); // the image is animated if it has a animated class
}
return false;
}
/**
* Generate amp-image tag of attachment post
*
* @param WP_Post $attachment attachment post
*
* @since 1.0.0
*
* @return string
*/
public function print_attachment_image($attachment) {
return $this->get_attachment_image($attachment->ID);
}
/**
* Get an HTML img element representing an image attachment
*
* todo fix default size
*
* @see wp_get_attachment_image for more documentation
*
* @param int $attachment_id Image attachment ID.
* @param string|array $size Optional. Image size. Accepts any valid image size, or an array of width
* and height values in pixels (in that order). Default 'full'.
* @param bool $icon Optional. Whether the image should be treated as an icon. Default false.
* @param string|array $attr Optional. Attributes for the image markup. Default empty.
*
* @since 1.0.0
*
* @return string HTML img element or empty string on failure.
*/
public function get_attachment_image($attachment_id, $size = 'full', $icon = false, $attr = '') {
$html = '';
$image = wp_get_attachment_image_src($attachment_id, $size, $icon);
if ($image) {
list( $src, $width, $height ) = $image;
$hwstring = image_hwstring($width, $height);
$size_class = $size;
if (is_array($size_class)) {
$size_class = join('x', $size_class);
}
$attachment = get_post($attachment_id);
$default_attr = array(
'src' => $src,
'class' => "attachment-$size_class size-$size_class",
'alt' => trim(strip_tags(get_post_meta($attachment_id, '_wp_attachment_image_alt', true))), // Use Alt field first
);
if( empty( $default_attr['alt']) ) {
$default_attr['alt'] = trim(strip_tags($attachment->post_excerpt));
} // If not, Use the Caption
if( empty( $default_attr['alt'] ) ) {
$default_attr['alt'] = trim(strip_tags($attachment->post_title));
} // Finally, use the title
$attr = wp_parse_args($attr, $default_attr);
// Generate 'srcset' and 'sizes' if not already present.
if( empty( $attr['srcset'] ) ) {
$image_meta = get_post_meta($attachment_id, '_wp_attachment_metadata', true);
if (is_array($image_meta)) {
$size_array = array(absint($width), absint($height));
$srcset = wp_calculate_image_srcset($size_array, $src, $image_meta, $attachment_id);
$sizes = wp_calculate_image_sizes($size_array, $src, $image_meta, $attachment_id);
if ($srcset && ( $sizes || !empty($attr['sizes']) )) {
$attr['srcset'] = $srcset;
if (empty($attr['sizes'])) {
$attr['sizes'] = $sizes;
}
}
}
}
/**
* Filters the list of attachment image attributes.
*
* @since 1.0.0
*
* @param array $attr Attributes for the image markup.
* @param WP_Post $attachment Image attachment post.
* @param string|array $size Requested size. Image size or array of width and height values
* (in that order). Default 'thumbnail'.
*/
$attr = apply_filters('wp_get_attachment_image_attributes', $attr, $attachment, $size);
$attr = $this->filter_attributes($attr, 'amp-img');
$attr = array_map('esc_attr', $attr);
$html = rtrim(" $value) {
$html .= " $name=" . '"' . $value . '"';
}
$html .= '>';
}
return $html;
}
/**
* This is our workaround to enforce max sizing with layout=responsive.
*
* We want elements to not grow beyond their width and shrink to fill the screen on viewports smaller than their
* width.
*
* See https://github.com/ampproject/amphtml/issues/1280#issuecomment-171533526
* See https://github.com/Automattic/amp-wp/issues/101
*
* @since 1.0.0
*
* @copyright credit goes to automattic amp - github.com/Automattic/amp-wp
*/
public function enforce_sizes_attribute($attributes) {
if (!isset($attributes['width'], $attributes['height'])) {
return $attributes;
}
$max_width = $attributes['width'];
if (( $_max_width = amp_wp_get_container_width() ) && $max_width > $_max_width) {
$max_width = $_max_width;
}
$attributes['sizes'] = sprintf('(min-width: %1$dpx) %1$dpx, 100vw', absint($max_width));
return $attributes;
}
/**
* Fetch remote image dimension
*
* @param string $url image url
*
* @see github.com/tommoor/fastimage
* @since 1.0.0
* @return bool|array array of width & height on success or false on error.
* array {
* 0 => image width
* 1 => image height
* }
*
*/
public function fetch_image_dimension($url) {
if( !class_exists('FastImage') ) {
require AMP_WP_DIR_PATH . 'includes/Fastimage.php';
}
$fast_image = new FastImage($url);
return $fast_image->getSize();
}
/**
* Get remote image dimension
*
* @param string $url
*
* @since 1.0.0
*
* @return bool|array array on success or false on error. @see fetch_image_dimension for more doc
*/
public function get_image_dimension($url) {
if (!( $url = $this->normalize_url($url) )) { return false; }
$url_hash = md5($url);
if( !( $dimension = get_transient( 'amp_wp_dimension_' . $url_hash ) ) ) {
if( $dimension = $this->fetch_image_dimension($url) ) {
set_transient('amp_wp_dimension_' . $url_hash, $dimension, HOUR_IN_SECONDS);
} else {
$dimension = array(
738, // fallback for width
400, // fallback for height
);
}
}
return $dimension;
}
/**
* @param string $url
*
* @since 1.0.0
* @copyright credit goes to automattic amp - github.com/Automattic/amp-wp
*
* @return bool|string url string on success
*/
public static function normalize_url($url) {
if( empty($url)) { return false; }
if( 0 === strpos($url, 'data:') ) { return false; }
if( 0 === strpos($url, '//') ) { return set_url_scheme($url, 'http'); }
$parsed = parse_url($url);
if( !isset($parsed['host']) ) {
$path = '';
if( isset($parsed['path']) ) { $path .= $parsed['path']; }
if( isset($parsed['query']) ) { $path .= '?' . $parsed['query']; }
$url = site_url($path);
}
return $url;
}
/**
* Change
tag to
*
* @since 1.0.0
*/
public function transform_image_tag_to_amp($html) {
return preg_replace('/<\s*img\s+/i', 'modify_attributes($image);
$is_anim = $this->is_animated_image_url($image['src']);
if ($is_anim) {
$this->enable_enqueue_scripts = true;
$tag_name = 'amp-anim';
} else {
$tag_name = 'amp-img';
}
$instance = new Amp_WP_Html_Util();
$node = $instance->create_node($tag_name, $image);
$output = $instance->saveXML($node, LIBXML_NOEMPTYTAG);
if ($echo) {
echo $output;
} else {
return $output;
}
}
/**
* Handy function to check image url is animated image or not
*
* @param string $url
*
* @since 1.0.0
*
* @return bool
*/
public function is_animated_image_url($url = '') {
$url = amp_wp_remove_query_string($url);
return strtolower(substr($url, - 4)) === '.gif';
}
}
// Register component class
amp_wp_register_component('Amp_WP_IMG_Component');
if( !function_exists('amp_wp_create_image')) {
/**
* Print AMP image from url
*
*
* @param $image
* @param bool $echo
*
* @since 1.0.0
*
* @return string
*/
function amp_wp_create_image($image, $echo = true) {
/**
* @var Amp_WP_IMG_Component $img_component
*/
$img_component = Amp_WP_Component::instance('Amp_WP_IMG_Component');
if ($echo) {
echo $img_component->create_image($image);
} else {
return $img_component->create_image($image);
}
}
}