env()->is_front) // if is front. { $this->initWebpHooks(); } } protected function initWebpHooks() { $webp_option = \wpSPIO()->settings()->deliverWebp; if ( $webp_option ) { // @tood Replace this function with the one in ENV. if(UtilHelper::shortPixelIsPluginActive('shortpixel-adaptive-images/short-pixel-ai.php')) { Notices::addWarning(__('Please deactivate the ShortPixel Image Optimizer\'s Deliver the next generation versions of the images in the front-end option when the ShortPixel Adaptive Images plugin is active.','shortpixel-image-optimiser'), true); } elseif( $webp_option == self::WEBP_GLOBAL ){ //add_action( 'wp_head', array($this, 'addPictureJs') ); // adds polyfill JS to the header || Removed. Browsers without picture support? add_action( 'init', array($this, 'startOutputBuffer'), 1 ); // start output buffer to capture content } elseif ($webp_option == self::WEBP_WP){ add_filter( 'the_content', array($this, 'convertImgToPictureAddWebp'), 10000 ); // priority big, so it will be executed last add_filter( 'the_excerpt', array($this, 'convertImgToPictureAddWebp'), 10000 ); add_filter( 'post_thumbnail_html', array($this,'convertImgToPictureAddWebp') ); } } } /* Picture generation, hooked on the_content filter * @param $content String The content to check and convert * @return String Converted content */ public function convertImgToPictureAddWebp($content) { if(function_exists('is_amp_endpoint') && is_amp_endpoint()) { //for AMP pages the tag is not allowed // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form return $content . (isset($_GET['SHORTPIXEL_DEBUG']) ? '' : ''); } $content = $this->convert($content); return $content; } public function startOutputBuffer() { $env = wpSPIO()->env(); if ($env->is_admin || $env->is_ajaxcall) return; $call = array($this, 'convertImgToPictureAddWebp'); ob_start( $call ); } protected function convert($content) { // Don't do anything with the RSS feed. if (is_feed() || is_admin()) { Log::addInfo('SPDBG convert is_feed or is_admin'); return $content; // . (isset($_GET['SHORTPIXEL_DEBUG']) ? '' : ''); } $new_content = $this->testPictures($content); if ($new_content !== false) { $content = $new_content; } else { Log::addDebug('Test Pictures returned empty.'); } if (! class_exists('DOMDocument')) { Log::addWarn('Webp Active, but DomDocument class not found ( missing xmldom library )'); return false; } // preg_match_all $content = preg_replace_callback('/]*>/i', array($this, 'convertImage'), $content); // [BS] No callback because we need preg_match_all $content = $this->testInlineStyle($content); return $content; } /** If lazy loading is happening, get source (src) from those values * Otherwise pass back image data in a regular way. */ private function lazyGet($img, $type) { $value = false; $prefix = false; if (isset($img['data-lazy-' . $type]) && strlen($img['data-lazy-' . $type]) > 0) { $value = $img['data-lazy-' . $type]; $prefix = 'data-lazy-'; } elseif( isset($img['data-' . $type]) && strlen($img['data-' . $type]) > 0) { $value = $img['data-' . $type]; $prefix = 'data-'; } elseif(isset($img[$type]) && strlen($img[$type]) > 0) { $value = $img[$type]; $prefix = ''; } return array( 'value' => $value, 'prefix' => $prefix, ); } /* Find image tags within picture definitions and make sure they are converted only by block, */ private function testPictures($content) { // [BS] Escape when DOM Module not installed //if (! class_exists('DOMDocument')) // return false; //$pattern ='' //$pattern ='/(?<=())(.*)(?=(<\/picture>))/mi'; $pattern = '/.*?().*?<\/picture>/is'; $count = preg_match_all($pattern, $content, $matches); if ($matches === false) return false; if ( is_array($matches) && count($matches) > 0) { foreach($matches[1] as $match) { $imgtag = $match; if (strpos($imgtag, 'class=') !== false) // test for class, if there, insert ours in there. { $pos = strpos($imgtag, 'class='); $pos = $pos + 7; $newimg = substr($imgtag, 0, $pos) . 'sp-no-webp ' . substr($imgtag, $pos); } else { $pos = 4; $newimg = substr($imgtag, 0, $pos) . ' class="sp-no-webp" ' . substr($imgtag, $pos); } $content = str_replace($imgtag, $newimg, $content); } } return $content; } /* This might be a future solution for regex callbacks. public static function processImageNode($node, $type) { $srcsets = $node->getElementsByTagName('srcset'); $srcs = $node->getElementsByTagName('src'); $imgs = $node->getElementsByTagName('img'); } */ /** Callback function with received an tag match * @param $match Image declaration block * @return String Replacement image declaration block */ protected function convertImage($match) { $fs = \wpSPIO()->filesystem(); $raw_image = $match[0]; // Raw Image HTML $image = new FrontImage($raw_image); if (false === $image->isParseable()) { return $raw_image; } $srcsetWebP = array(); $srcsetAvif = array(); // Count real instances of either of them, without fillers. $webpCount = $avifCount = 0; $imagePaths = array(); $definitions = $image->getImageData(); $imageBase = $image->getImageBase(); foreach ($definitions as $definition) { // Split the URL from the size definition ( eg 800w ) $parts = preg_split('/\s+/', trim($definition)); $image_url = $parts[0]; // The space if not set is required, otherwise it will not work. $image_condition = isset($parts[1]) ? ' ' . $parts[1] : ' '; // A source that starts with data:, will not need processing. if (strpos($image_url, 'data:') === 0) { continue; } $fsFile = $fs->getFile($image_url); $extension = $fsFile->getExtension(); // trigger setFileinfo, which will resolve URL -> Path $mime = $fsFile->getMime(); // Can happen when file is virtual, or other cases. Just assume this type. if ($mime === false) { $mime = 'image/' . $extension; } $fileWebp = $fs->getFile($imageBase . $fsFile->getFileBase() . '.webp'); $fileWebpCompat = $fs->getFile($imageBase . $fsFile->getFileName() . '.webp'); // The URL of the image without the filename $image_url_base = str_replace($fsFile->getFileName(), '', $image_url); $files = array($fileWebp, $fileWebpCompat); $fileAvif = $fs->getFile($imageBase . $fsFile->getFileBase() . '.avif'); $lastwebp = false; foreach($files as $index => $thisfile) { if (! $thisfile->exists()) { // FILTER: boolean, object, string, filedir $thisfile = $fileWebp_exists = apply_filters('shortpixel/front/webp_notfound', false, $thisfile, $image_url, $imageBase); } if ($thisfile !== false) { // base url + found filename + optional condition ( in case of sourceset, as in 1400w or similar) $webpCount++; $lastwebp = $image_url_base . $thisfile->getFileName() . $image_condition; $srcsetWebP[] = $lastwebp; break; } elseif ($index+1 !== count($files)) // Don't write the else on the first file, because then the srcset will be written twice ( if file exists on the first fails) { continue; } else { $lastwebp = $definition; $srcsetWebP[] = $lastwebp; } } if (false === $fileAvif->exists()) { $fileAvif = apply_filters('shortpixel/front/webp_notfound', false, $fileAvif, $image_url, $imageBase); } if ($fileAvif !== false) { $srcsetAvif[] = $image_url_base . $fileAvif->getFileName() . $image_condition; $avifCount++; } else { //fallback to jpg if (false !== $lastwebp) // fallback to webp if there is a variant in this run. or jpg if none { $srcsetAvif[] = $lastwebp; } else { $srcsetAvif[] = $definition; } } } if ($webpCount == 0 && $avifCount == 0) { return $raw_image; } $args = array(); if ($webpCount > 0) $args['webp'] = $srcsetWebP; if ($avifCount > 0) $args['avif'] = $srcsetAvif; $output = $image->parseReplacement($args); return $output; } protected function testInlineStyle($content) { //preg_match_all('/background.*[^:](url\(.*\))[;]/isU', $content, $matches); preg_match_all('/url\(.*\)/isU', $content, $matches); if (count($matches) == 0) return $content; $content = $this->convertInlineStyle($matches, $content); return $content; } /** Function to convert inline CSS backgrounds to webp * @param $match Regex match for inline style * @return String Replaced (or not) content for webp. * @author Bas Schuiling */ protected function convertInlineStyle($matches, $content) { $fs = \wpSPIO()->filesystem(); $allowed_exts = array('jpg', 'jpeg', 'gif', 'png'); $converted = array(); for($i = 0; $i < count($matches[0]); $i++) { $item = $matches[0][$i]; preg_match('/url\(\'(.*)\'\)/imU', $item, $match); if (! isset($match[1])) continue; $url = $match[1]; //$parsed_url = parse_url($url); $filename = basename($url); $fileonly = pathinfo($url, PATHINFO_FILENAME); $ext = pathinfo($url, PATHINFO_EXTENSION); if (! in_array($ext, $allowed_exts)) continue; $image_base_url = str_replace($filename, '', $url); $fsFile = $fs->getFile($url); $dir = $fsFile->getFileDir(); $imageBase = is_object($dir) ? $dir->getPath() : false; if (false === $imageBase) // returns false if URL is external, do nothing with that. continue; $checkedFile = false; $fileWebp = $fs->getFile($imageBase . $fsFile->getFileBase() . '.webp'); $fileWebpCompat = $fs->getFile($imageBase . $fsFile->getFileName() . '.webp'); if (true === $fileWebp->exists()) { $checkedFile = $image_base_url . $fsFile->getFileBase() . '.webp'; } elseif (true === $fileWebpCompat->exists()) { $checkedFile = $image_base_url . $fsFile->getFileName() . '.webp'; } else { $fileWebp_exists = apply_filters('shortpixel/front/webp_notfound', false, $fileWebp, $url, $imageBase); if (false !== $fileWebp_exists) { $checkedFile = $image_base_url . $fsFile->getFileBase() . '.webp'; } else { $fileWebp_exists = apply_filters('shortpixel/front/webp_notfound', false, $fileWebpCompat, $url, $imageBase); if (false !== $fileWebp_exists) { $checkedFile = $image_base_url . $fsFile->getFileName() . '.webp'; } } } if ($checkedFile) { // if webp, then add another URL() def after the targeted one. (str_replace old full URL def, with new one on main match? $target_urldef = $matches[0][$i]; if (! isset($converted[$target_urldef])) // if the same image is on multiple elements, this replace might go double. prevent. { $converted[] = $target_urldef; $new_urldef = "url('" . $checkedFile . "'), " . $target_urldef; $content = str_replace($target_urldef, $new_urldef, $content); } } } return $content; } } // class