File: /home/wynnelaw5142/www/wp-content/plugins/tiny-compress-images/src/class-tiny-picture.php
<?php
/*
* Tiny Compress Images - WordPress plugin.
* Copyright (C) 2015-2018 Tinify B.V.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/**
* Class responsible for parsing and modifying html to insert picture elements.
*
* 1) searches for <picture> elements or <img> elements
* 2) checks wether existing source has a modern alternative
* 3) augments or creates a picture element
* 4) replaces the original source with the source which includes the modern format
*/
class Tiny_Picture extends Tiny_WP_Base {
/** @var string */
private $base_dir;
/** @var array */
private $allowed_domains = array();
/**
* Initialize the plugin.
*
* @param string $base_dir Absolute path (e.g. ABSPATH)
* @param array $domains List of allowed domain URLs
*/
function __construct( $base_dir = ABSPATH, $domains = array() ) {
$this->base_dir = $base_dir;
$this->allowed_domains = $domains;
if ( is_admin() || is_customize_preview() ) {
return;
}
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
return;
}
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
return;
}
if ( Tiny_Helpers::is_pagebuilder_request() ) {
return;
}
add_action('template_redirect', function () {
ob_start( array( $this, 'replace_sources' ), 1000 );
});
}
public function replace_sources( $content ) {
$content = $this->replace_picture_sources( $content );
$content = $this->replace_img_sources( $content );
return $content;
}
/**
* Will extend existing picture elements with additional sourcesets
*
* @param string $content
* @return string the new source html
*/
private function replace_picture_sources( $content ) {
$picture_sources = $this->filter_pictures( $content );
foreach ( $picture_sources as $picture_source ) {
$content = $this->replace_picture( $content, $picture_source );
}
return $content;
}
private function replace_img_sources( $content ) {
$image_sources = $this->filter_images( $content );
foreach ( $image_sources as $image_source ) {
$content = Tiny_Picture::replace_image( $content, $image_source );
}
return $content;
}
/**
* Will search for all picture elements within the given source html
*
* @param string $content
* @return array<Tiny_Picture_Source> an array of picture element sources
*/
private function filter_pictures( $content ) {
$matches = array();
if ( ! preg_match_all(
'#<picture\b[^>]*>.*?<\/picture>#is',
$content,
$matches
) ) {
return array();
}
$pictures = array();
foreach ( $matches[0] as $raw_picture ) {
$pictures[] = new Tiny_Picture_Source(
$raw_picture,
$this->base_dir,
$this->allowed_domains
);
}
return $pictures;
}
/**
* Will add additional sourcesets to picture elements.
*
* @param string $content the full page content
* @param Tiny_Picture_Source $source the picture element
*
* @return string the updated content including augmented picture elements
*/
private function replace_picture( $content, $source ) {
$content = str_replace( $source->raw_html, $source->augment_picture_element(), $content );
return $content;
}
/**
* Will replace img elements with picture elements that (possibly) have additional formats.
*
* @param string $content the full page content
* @param Tiny_Image_Source $source the picture element
*
* @return string the updated content including augmented picture elements
*/
private static function replace_image( $content, $source ) {
$content = str_replace( $source->raw_html, $source->create_picture_elements(), $content );
return $content;
}
/**
* Filters out all images from the content and returns them as an array.
*
* @return Tiny_Image[]
*/
private function filter_images( $content ) {
// Extract only the <body>...</body> section.
if ( preg_match( '/(?=<body).*<\/body>/is', $content, $body ) ) {
$content = $body[0];
}
// strip HTML comments.
$content = preg_replace( '/<!--(.*)-->/Uis', '', $content );
// strip existing <picture> blocks to avoid double-processing.
$content = preg_replace( '/<picture\b.*?>.*?<\/picture>/is', '', $content );
// Strip <noscript> blocks to avoid altering their contents.
$content = preg_replace( '/<noscript\b.*?>.*?<\/noscript>/is', '', $content );
// Find all <img> tags with any attributes.
if ( ! preg_match_all( '/<img\b[^>]*>/is', $content, $matches ) ) {
return array();
}
$images = array();
foreach ( $matches[0] as $img ) {
$images[] = new Tiny_Image_Source(
$img,
$this->base_dir,
$this->allowed_domains
);
}
return $images;
}
}
abstract class Tiny_Source_Base {
public $raw_html;
protected $base_dir;
protected $allowed_domains;
protected $valid_mimetypes;
public function __construct( $html, $base_dir, $domains ) {
$this->raw_html = $html;
$this->base_dir = $base_dir;
$this->allowed_domains = $domains;
$this->valid_mimetypes = array( 'image/avif', 'image/webp' );
}
protected static function get_attribute_value( $element, $name ) {
// Match the exact attribute name (not part of data-media, mediaType, etc.)
// and capture a single- or double-quoted value.
$delim = '~';
$attr = preg_quote( $name, $delim );
$regex = $delim . '(?<![\w:-])' . $attr . '\s*=\s*(["\'])(.*?)\1' . $delim . 'is';
if ( preg_match( $regex, $element, $m ) ) {
return $m[2];
}
return null;
}
/**
* Extract elements by tag name from an HTML string (regex-based).
*
* @param string $html The HTML string to search in.
* @param string $tagname The tag name (e.g., 'div', 'source', 'img').
* @return array Array of matched elements as strings.
*/
protected function get_element_by_tag( $html, $tagname ) {
$results = [];
// Self-closing / void tag (e.g. <source />, <img />, <br />)
if ( preg_match_all(
'~<' . preg_quote( $tagname, '~' ) . '\b(?:[^>"\']+|"[^"]*"|\'[^\']*\')*/?>~i',
$html,
$matches
) ) {
$results = array_merge( $results, $matches[0] );
}
// Normal paired tags (e.g. <div>…</div>)
$regex_tag = preg_quote( $tagname, '~' );
if ( preg_match_all(
'~<' . $regex_tag .
'\b(?:[^>"\']+|"[^"]*"|\'[^\']*\')*>.*?</' .
$regex_tag .
'>~is',
$html,
$matches
) ) {
$results = array_merge( $results, $matches[0] );
}
return $results;
}
protected function get_local_path( $url ) {
if ( strpos( $url, 'http' ) === 0 ) {
$matched_domain = null;
foreach ( $this->allowed_domains as $domain ) {
if ( strpos( $url, $domain ) === 0 ) {
$matched_domain = $domain;
break;
}
}
if ( null === $matched_domain ) {
return '';
}
$url = substr( $url, strlen( $matched_domain ) );
}
$url = $this->base_dir . $url;
return $url;
}
protected function get_formatted_source( $image_source_data, $mimetype ) {
$format_url = Tiny_Helpers::replace_file_extension( $mimetype, $image_source_data['path'] );
$local_path = $this->get_local_path( $format_url );
if ( empty( $local_path ) ) {
return null;
}
$exists_local = file_exists( $local_path );
if ( $exists_local ) {
return array(
'src' => $format_url,
'size' => $image_source_data['size'],
'type' => $mimetype,
);
}
return null;
}
/**
* Retrieves the sources from the <img> or <source> element
*
* @return array{path: string, size: string}[] The image sources
*/
protected function get_image_srcsets( $html ) {
$result = array();
$srcset = $this::get_attribute_value( $html, 'srcset' );
if ( $srcset ) {
// Split the srcset to get individual entries
$srcset_entries = explode( ',', $srcset );
foreach ( $srcset_entries as $entry ) {
// Trim whitespace
$entry = trim( $entry );
// Split by whitespace to separate path and size/density descriptor
$parts = preg_split( '/\s+/', $entry, 2 );
if ( count( $parts ) === 2 ) {
// We have both path and size
$result[] = array(
'path' => $parts[0],
'size' => $parts[1],
);
} elseif ( count( $parts ) === 1 ) {
// We only have a path, will be interpreted as pixel
// density 1x (unusual in srcset)
$result[] = array(
'path' => $parts[0],
'size' => '',
);
}
}
}
return $result;
}
/**
* Retrieves the sources from the <img> or <source> element
*
* @return array{path: string, size: string}[] The image sources
*/
private function get_image_src( $html ) {
$source = $this::get_attribute_value( $html, 'src' );
if ( ! empty( $source ) ) {
// No srcset, but we have a src attribute
return array(
'path' => $source,
'size' => '',
);
}
return array();
}
/**
* Creates one or more <source> elements if alternative formats
* are available.
*
* @param string $original_source_html, either <source> or <img>
* @return array{string} array of <source> html
*/
protected function create_alternative_sources( $original_source_html ) {
$srcsets = $this->get_image_srcsets( $original_source_html );
if ( empty( $srcsets ) ) {
// no srcset, try src attribute
$srcsets[] = $this->get_image_src( $original_source_html );
}
if ( empty( $srcsets ) ) {
return array();
}
$is_source_tag = (bool) preg_match( '#<source\b#i', $original_source_html );
$sources = array();
$width_descriptor = $this->get_largest_width_descriptor( $srcsets );
foreach ( $this->valid_mimetypes as $mimetype ) {
$srcset_parts = array();
foreach ( $srcsets as $srcset ) {
$alt_source = $this->get_formatted_source( $srcset, $mimetype );
if ( $alt_source ) {
$srcset_parts[] = trim( $alt_source['src'] . ' ' . $alt_source['size'] );
}
}
if (
$width_descriptor &&
! self::srcset_contains_width_descriptor(
$srcset_parts,
$width_descriptor
)
) {
continue;
}
if ( empty( $srcset_parts ) ) {
continue;
}
$source_attr_parts = array();
$srcset_attr = implode( ', ', $srcset_parts );
$source_attr_parts['srcset'] = $srcset_attr;
if ( $is_source_tag ) {
foreach ( array( 'sizes', 'media', 'width', 'height' ) as $attr ) {
$attr_value = $this->get_attribute_value( $original_source_html, $attr );
if ( $attr_value ) {
$source_attr_parts[ $attr ] = $attr_value;
}
}
}
$source_attr_parts['type'] = $mimetype;
$source_parts = array( '<source' );
foreach ( $source_attr_parts as $source_attr_name => $source_attr_val ) {
$source_parts[] = $source_attr_name . '="' . $source_attr_val . '"';
}
$source_parts[] = '/>';
$sources[] = implode( ' ', $source_parts );
} // End foreach().
return $sources;
}
/**
* Returns the largest numeric width descriptor
* (e.g. 2000 from "2000w") found in the srcset data.
*
* @param array<array{path: string, size: string}> $srcsets
* @return int
*/
public static function get_largest_width_descriptor( $srcsets ) {
$largest = 0;
foreach ( $srcsets as $srcset ) {
if ( empty( $srcset['size'] ) ) {
continue;
}
if ( preg_match( '/(\d+)w/', $srcset['size'], $matches ) ) {
$width = (int) $matches[1];
if ( $width > $largest ) {
$largest = $width;
}
}
}
return $largest;
}
/**
* Determines whether a srcset list contains the provided width descriptor.
*
* @param string[] $srcset_parts
* @param int $width_descriptor
* @return bool true if width is in srcset
*/
public static function srcset_contains_width_descriptor( $srcset_parts, $width_descriptor ) {
if ( empty( $srcset_parts ) || $width_descriptor <= 0 ) {
return false;
}
$suffix = ' ' . $width_descriptor . 'w';
$suffix_length = strlen( $suffix );
foreach ( $srcset_parts as $srcset_part ) {
if ( substr( $srcset_part, -$suffix_length ) === $suffix ) {
return true;
}
}
return false;
}
}
class Tiny_Picture_Source extends Tiny_Source_Base {
/**
* Adds alternative format sources (e.g., image/webp, image/avif) to an existing
* <picture> element based on locally available converted files.
*
* @return string The augmented <picture> HTML or the original if no additions.
*/
public function augment_picture_element() {
$modified_sources = array();
foreach ( $this->get_element_by_tag( $this->raw_html, 'source' ) as $source_tag_html ) {
$type_attr = self::get_attribute_value( $source_tag_html, 'type' );
$type_attr = null !== $type_attr ? strtolower( trim( $type_attr ) ) : '';
// Skip if already optimized.
if ( '' !== $type_attr && in_array( $type_attr, $this->valid_mimetypes, true ) ) {
continue;
}
$alternative_sources = $this->create_alternative_sources( $source_tag_html );
if ( is_array( $alternative_sources ) && $alternative_sources ) {
foreach ( $alternative_sources as $alt ) {
$modified_sources[] = $alt; // no array_merge in the loop
}
}
}
// handle inner image
foreach ( $this->get_element_by_tag( $this->raw_html, 'img' ) as $img_tag_html ) {
$alt_image_source = $this->create_alternative_sources( $img_tag_html );
$modified_sources = array_merge( $modified_sources, $alt_image_source );
}
$modified_source = implode( '', $modified_sources );
// Insert newly built <source> elements immediately before the first <img>
return preg_replace( '#(<img\b)#i', $modified_source . '$1', $this->raw_html, 1 );
}
}
class Tiny_Image_Source extends Tiny_Source_Base {
/**
* Generates a formatted image source array if the corresponding local file exists.
*
* Attempts to replace the file extension of the provided image path with the
* specified MIME type, resolves the local path of the resulting file, and returns
* the `srcset` and `type` if the file exists.
*
* @return string a <picture> element contain additional sources
*/
public function create_picture_elements() {
$sources = $this->create_alternative_sources( $this->raw_html );
if ( empty( $sources ) ) {
return $this->raw_html;
}
$picture_element = array( '<picture>' );
$picture_element[] = implode( '', $sources );
$picture_element[] = $this->raw_html;
$picture_element[] = '</picture>';
return implode( '', $picture_element );
}
}