PluginProbe ʕ •ᴥ•ʔ
10Web Booster – Website speed optimization, Cache & Page Speed optimizer / trunk
10Web Booster – Website speed optimization, Cache & Page Speed optimizer vtrunk
2.33.0 2.30.5 2.30.7 2.30.9 2.31.10 2.31.8 2.32.11 2.32.21 2.32.3 2.32.4 2.32.7 2.6.31 2.6.40 2.6.42 2.6.7 2.7.37 2.7.44 2.7.47 2.8.18 2.8.19 2.8.32 2.8.34 2.8.35 2.9.23 2.9.24 2.9.25 2.9.27 v2.27.4 trunk 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.17 2.0.18 2.0.21 2.0.22 2.0.25 2.0.26 2.0.27 2.0.3 2.0.7 2.0.9 2.10.46 2.10.65 2.10.66 2.10.68 2.11.41 2.11.42 2.11.43 2.12.15 2.12.21 2.12.22 2.12.23 2.12.26 2.13.37 2.13.40 2.13.41 2.13.42 2.13.44 2.13.45 2.13.47 2.14.49 2.14.50 2.15.18 2.17.21 2.17.23 2.18.17 2.19.44 2.19.45 2.19.46 2.19.49 2.2.12 2.2.15 2.2.16 2.2.18 2.2.8 2.20.31 2.20.32 2.20.33 2.21.11 2.21.12 2.21.16 2.21.25 2.22.32 2.23.13 2.23.15 2.23.16 2.23.18 2.24.12 2.24.14 2.24.18 2.25.14 2.26.6 2.28.10 2.28.13 2.28.14 2.28.7 2.29.1 2.29.2 2.29.3 2.3.0 2.3.1 2.3.2 2.3.3 2.30.18
tenweb-speed-optimizer / includes / OptimizerImages.php
tenweb-speed-optimizer / includes Last commit date
WebPageCache 8 months ago external 2 months ago ExcludeJsFromDelay.php 2 years ago OptimizerBanner.php 11 months ago OptimizerBase.php 2 years ago OptimizerCSSMin.php 2 years ago OptimizerCache.php 1 year ago OptimizerCacheStructure.php 2 years ago OptimizerCriticalCss.php 1 year ago OptimizerElementor.php 2 years ago OptimizerFonts.php 2 years ago OptimizerImages.php 1 year ago OptimizerLogger.php 1 year ago OptimizerMain.php 2 months ago OptimizerOnInit.php 11 months ago OptimizerScripts.php 2 years ago OptimizerSettings.php 2 months ago OptimizerStyles.php 2 years ago OptimizerUrl.php 1 year ago OptimizerUtils.php 2 months ago OptimizerWhiteLabel.php 2 years ago
OptimizerImages.php
1050 lines
1 <?php
2
3 namespace TenWebOptimizer;
4
5 /*
6 * Handles optimizing images.
7 */
8 if (!defined('ABSPATH')) {
9 exit;
10 }
11
12 class OptimizerImages
13 {
14 const TWO_SRC_ATTRIBUTE_VALUE_REGEXP = 'src=["\']([^"\'].+?)["\']|srcset=["\']([^"\'].+?)["\']';
15
16 const TWO_TYPE_ATTRIBUTE_VALUE_REGEXP = 'type=["\']([^"\'].+?)["\']';
17
18 const TWO_AUDIO_MARKER = 'TWO_AUDIO_MARKER ';
19
20 const TWO_EXCLUDE_LAZY_KEYWORDS = [
21 'image-compare__'
22 ];
23
24 /**
25 * Options.
26 *
27 * @var array
28 */
29 private $two_iframe_lazyload = 'off';
30
31 private $two_youtube_vimeo_iframe_lazyload = 'off';
32
33 private $browser_lazy = false;
34
35 private $vanilla_lazy = false;
36
37 private $lazy_class = 'lazyload ';
38
39 private $smart_lazy_load_data = [];
40
41 /**
42 * Singleton instance.
43 *
44 * @var self|null
45 */
46 protected static $instance = null;
47
48 private $TwoSettings;
49
50 public function __construct($options = [])
51 {
52 global $TwoSettings;
53 $this->TwoSettings = $TwoSettings;
54 $lazy_load_type = $this->TwoSettings->get_settings('lazy_load_type');
55
56 if ($lazy_load_type === 'browser') {
57 $this->browser_lazy = true;
58 }
59
60 if ($lazy_load_type === 'vanilla') {
61 $this->vanilla_lazy = true;
62 $this->lazy_class = 'lazy ';
63 }
64
65 if (isset($options['two_iframe_lazyload'])) {
66 $this->two_iframe_lazyload = $options['two_iframe_lazyload'];
67 }
68
69 if (isset($options['two_youtube_vimeo_iframe_lazyload'])) {
70 $this->two_youtube_vimeo_iframe_lazyload = $options['two_youtube_vimeo_iframe_lazyload'];
71 }
72 }
73
74 public static function instance($options = [])
75 {
76 if (null === self::$instance) {
77 self::$instance = new self($options);
78 }
79
80 return self::$instance;
81 }
82
83 public function run()
84 {
85 $this->run_on_frontend();
86 }
87
88 public function run_on_frontend()
89 {
90 add_filter('twoptimize_html_after_minify', [$this, 'filter_lazyload_images'], 10, 1);
91 add_filter('twoptimize_html_after_minify_iframe', [$this, 'filter_lazyload_iframes'], 10, 1);
92 add_filter('twoptimize_html_after_minify_video', [$this, 'filter_lazyload_video'], 10, 1);
93 add_filter('twoptimize_html_images', [$this, 'filter_optimize_html_images'], 10, 1);
94 }
95
96 public function get_size_from_tag($tag)
97 {
98 // reusable function to extract widht and height from an image tag
99 // enforcing a filterable maximum width and height (default 4999X4999).
100 $width = '';
101 $height = '';
102
103 if (preg_match('#width=("|\')(.*)("|\')#Usmi', $tag, $_width)) {
104 if (strpos($_width[2], '%') === false) {
105 $width = (int) $_width[2];
106 }
107 }
108
109 if (preg_match('#height=("|\')(.*)("|\')#Usmi', $tag, $_height)) {
110 if (strpos($_height[2], '%') === false) {
111 $height = (int) $_height[2];
112 }
113 }
114 // check for and enforce (filterable) max sizes.
115 $_max_width = 4999;
116
117 if ($width > $_max_width) {
118 $_width = $_max_width;
119 $height = $_width / $width * $height;
120 $width = $_width;
121 }
122 $_max_height = 4999;
123
124 if ($height > $_max_height) {
125 $_height = $_max_height;
126 $width = $_height / $height * $width;
127 $height = $_height;
128 }
129
130 return [
131 'width' => $width,
132 'height' => $height,
133 ];
134 }
135
136 public function should_lazyload($context = '')
137 {
138 return true;
139 }
140
141 public function filter_optimize_html_images($in)
142 {
143 $to_replace = [];
144 // hide (no)script tags to avoid nesting noscript tags (as lazyloaded images add noscript).
145 $out = OptimizerBase::replace_contents_with_marker_if_exists('SCRIPT', '<script', '#<(?:no)?script.*?<\/(?:no)?script>#is', $in);
146
147 // extract img tags and add lazyload attribs.
148 if (preg_match_all('#(<img[^>]*src[^>]*>)#Usmi', $out, $matches)) {
149 foreach ($matches[0] as $tag) {
150 // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
151 //if ( $this->should_lazyload($out) ) {
152 $to_replace[$tag] = $this->disable_pagespeed_image_optimization($tag);
153 // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
154 //}
155 }
156 $out = str_replace(array_keys($to_replace), array_values($to_replace), $out);
157 }
158 // restore noscript tags.
159 $out = OptimizerBase::restore_marked_content('SCRIPT', $out);
160
161 return $out;
162 }
163
164 public function filter_lazyload_images($in)
165 {
166 $to_replace = [];
167 // hide (no)script tags to avoid nesting noscript tags (as lazyloaded images add noscript).
168 $out = OptimizerBase::replace_contents_with_marker_if_exists('SCRIPT', '<script', '#<(?:no)?script.*?<\/(?:no)?script>#is', $in);
169 // extract img tags and add lazyload attribs.
170
171 preg_match_all('#<audio\s*.*>\s*.*<\/audio>#Usmi', $out, $matches);
172 $audio_to_replace = [];
173
174 foreach ($matches as $tag) {
175 if (!empty($tag[0])) {
176 $audio = $tag[0];
177 $audio_to_replace[$audio] = str_replace('<source ', '<source ' . self::TWO_AUDIO_MARKER, $audio);
178 }
179 }
180
181 $out = str_replace(array_keys($audio_to_replace), array_values($audio_to_replace), $out);
182
183 if (preg_match_all('#<img[^>]*src[^>]*>|<source[^>]*src[^>]*>#Usmi', $out, $matches)) {
184 foreach ($matches[0] as $tag) {
185 if ($this->should_lazyload($out) && strpos($tag, self::TWO_AUDIO_MARKER) === false) {
186 $to_replace[$tag] = $this->image_lazyload($tag);
187 }
188 }
189 $out = str_replace(array_keys($to_replace), array_values($to_replace), $out);
190 }
191 // restore noscript tags.
192 $out = OptimizerBase::restore_marked_content('SCRIPT', $out);
193 $out = str_replace(self::TWO_AUDIO_MARKER, '', $out);
194
195 return $out;
196 }
197
198 public function filter_lazyload_iframes($in)
199 {
200
201 // only used is image optimization is NOT active but lazyload is.
202 $to_replace = [];
203 // hide (no)script tags to avoid nesting noscript tags (as lazyloaded images add noscript).
204 $out = OptimizerBase::replace_contents_with_marker_if_exists('SCRIPT', '<script', '#<(?:no)?script.*?<\/(?:no)?script>#is', $in);
205 // extract img tags and add lazyload attribs.
206
207 if (preg_match_all('#<iframe[^>]*src[^>]*>#Usmi', $out, $matches)) {
208 // only used is image optimization is NOT active but lazyload is.
209 foreach ($matches[0] as $tag) {
210 if ($this->should_lazyload($out)) {
211 $to_replace[$tag] = $this->add_lazyload($tag);
212 }
213 }
214
215 $out = str_replace(array_keys($to_replace), array_values($to_replace), $out);
216 }
217 // restore noscript tags.
218 $out = OptimizerBase::restore_marked_content('SCRIPT', $out);
219
220 return $out;
221 }
222
223 public function filter_lazyload_video($in)
224 {
225
226 // only used is image optimization is NOT active but lazyload is.
227 $to_replace = [];
228 // hide (no)script tags to avoid nesting noscript tags (as lazyloaded images add noscript).
229 $out = OptimizerBase::replace_contents_with_marker_if_exists('SCRIPT', '<script', '#<(?:no)?script.*?<\/(?:no)?script>#is', $in);
230
231 // extract img tags and add lazyload attribs.
232 //<video[^>]*>(.*?)</video>
233 if (preg_match_all('#<video[^>]*>((.*?\n*+)+)<\/video>#', $out, $matches)) {
234 // only used is image optimization is NOT active but lazyload is.
235 foreach ($matches[0] as $tag) {
236 if ($this->should_lazyload($out)) {
237 $to_replace[$tag] = $this->add_lazyload($tag);
238 }
239 }
240 $out = str_replace(array_keys($to_replace), array_values($to_replace), $out);
241 }
242 // restore noscript tags.
243 $out = OptimizerBase::restore_marked_content('SCRIPT', $out);
244
245 return $out;
246 }
247
248 public function add_lazyload($tag, $placeholder = '')
249 {
250 if ($this->isExcludedTag($tag)) {
251 return $tag;
252 }
253
254 if (empty($this->TwoSettings->get_settings('two_lazyload_slider_images', ''))) {
255 /*
256 * keywords for popular sliders
257 * exclude slider images
258 * */
259 $slider_keywords = [
260 'swiper-slide',
261 'slider',
262 'soliloquy',
263 'rev-slide',
264 'rev-slidebg'
265 ];
266
267 foreach ($slider_keywords as $val) {
268 if (strpos($tag, $val) !== false) {
269 return $tag;
270 }
271 }
272 }
273
274 preg_match('@' . self::TWO_SRC_ATTRIBUTE_VALUE_REGEXP . '@', $tag, $match);
275 $src = array_pop($match);
276
277 if (!isset($src) || $src === '') {
278 return $tag;
279 }
280
281 if ($this->isExcluded($tag, 'two_exclude_lazyload')) {
282 return $tag;
283 }
284
285 /*
286 * end excluding images for sliders
287 * */
288
289 // adds actual lazyload-attributes to an image node.
290 if (str_ireplace($this->get_lazyload_exclusions(), '', $tag) === $tag) {
291
292 // store original tag for use in noscript version.
293 $noscript_tag = '<noscript>' . $tag . '</noscript>';
294
295 // insert lazyload class.
296 if (!$this->browser_lazy) {
297 //lazyload
298 $tag = self::inject_classes_in_tag($tag, $this->lazy_class);
299
300 if (!$placeholder || empty($placeholder)) {
301 $placeholder = $this->generatePlaceholder($tag);
302 }
303 }
304
305 if ($this->isIframe($tag)) {
306 $tag = $this->replace_iframe($tag, $src);
307 } elseif ($this->isVideo($tag)) {
308 $tag = $this->replace_video($tag);
309 } else {
310 $tag = $this->replace_image($tag, $placeholder);
311 }
312 $pos_srcset = strpos($tag, 'data-srcset=');
313
314 if (!$pos_srcset) {
315 $tag = str_replace('srcset=', ' data-srcset=', $tag);
316 }
317 $pos_sizes = strpos($tag, 'data-sizes=');
318
319 if (!$pos_sizes) {
320 $tag = str_replace('sizes=', ' data-sizes=', $tag);
321 }
322 // add the noscript-tag from earlier.
323 $two_add_noscript = empty($this->TwoSettings->get_settings('two_add_noscript', 'off')) ? 'off' : 'on';
324
325 if ($two_add_noscript == 'on') {
326 $tag = $noscript_tag . $tag;
327 }
328 }
329
330 return $tag;
331 }
332
333 private function generatePlaceholder($tag)
334 {
335 // get image width & heigth for placeholder fun (and to prevent content reflow).
336 $_get_size = $this->get_size_from_tag($tag);
337 $width = $_get_size['width'];
338 $height = $_get_size['height'];
339
340 if (false === $width) {
341 $widht = 210; // default width for SVG placeholder.
342 }
343
344 if (false === $height) {
345 $heigth = $width / 3 * 2; // if no height, base it on width using the 3/2 aspect ratio.
346 }
347 // insert the actual lazyload stuff.
348 // see https://css-tricks.com/preventing-content-reflow-from-lazy-loaded-images/ for great read on why we're using empty svg's.
349 $placeholder = $this->get_default_lazyload_placeholder($width, $height);
350
351 return $placeholder;
352 }
353
354 private function replace_iframe($tag, $src)
355 {
356 $video_id = null;
357 $class_name = null;
358
359 $this->smart_lazy_load_data('iframe');
360
361 if ($this->browser_lazy) {
362 $tag = str_replace('<iframe', '<iframe loading="lazy"', $tag);
363 } else {
364 if ($this->two_youtube_vimeo_iframe_lazyload === 'on') {
365 preg_match('%(?:youtube(?:-nocookie)?\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^"&?/ ]{11})%i', $src, $match);
366
367 if (!empty($match[1])) {
368 $video_id = $match[1];
369 $class_name = 'yt-lazyload';
370 } else {
371 preg_match('%^https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)(?:[?]?.*)$%im', $src, $match);
372
373 if (!empty($match[3])) {
374 $video_id = $match[3];
375 $class_name = 'vi-lazyload';
376 }
377 }
378 }
379
380 if ($video_id && $class_name) {
381 $tag = '<div class="' . $class_name . '" data-id="' . $video_id . '" data-thumb="" data-logo="2"></div>';
382 $this->smart_lazy_load_data('two_youtube_vimeo_iframe_lazyload');
383 } else {
384 $tag = str_replace('src=', ' src="" data-src=', $tag);
385 }
386 }
387
388 return $tag;
389 }
390
391 private function replace_video($tag)
392 {
393 $pos = strpos($tag, 'data-src=');
394
395 if ($this->browser_lazy) {
396 $tag = str_replace('<video', '<video preload="none"', $tag);
397 } elseif ($this->vanilla_lazy && !$pos) {
398 $tag = str_replace('src', 'data-src', $tag);
399
400 if (false === strpos($tag, 'data-poster=')) {
401 $tag = str_replace('poster=', 'data-poster=', $tag);
402 }
403 } elseif (!$pos) {
404 /* Get src from source tag*/
405 preg_match_all('@' . self::TWO_SRC_ATTRIBUTE_VALUE_REGEXP . '@', $tag, $match);
406 $src = array_pop($match);
407
408 /* Get type from source tag*/
409 preg_match_all('@' . self::TWO_TYPE_ATTRIBUTE_VALUE_REGEXP . '@', $tag, $match);
410 $type = array_pop($match);
411
412 /* Get data-src attr accoording lib requirements */
413 $data_src = '';
414 $srcCount = count($src);
415
416 for ($i = 0; $i < $srcCount; $i++) {
417 $t = explode('.', $src[$i]);
418
419 if (!isset($type[$i])) {
420 $type[$i] = 'video/' . end($t);
421 }
422 $data_src .= $src[$i] . '|' . $type[$i] . ',';
423 }
424 $data_src = rtrim($data_src, ',');
425
426 $tag = str_replace('<video', '<video data-src=' . $data_src, $tag);
427
428 if (false === strpos($tag, 'data-poster=')) {
429 $tag = str_replace('poster=', 'data-poster=', $tag);
430 }
431 /* Remove source tags inside the video tag */
432 $tag = preg_replace("/<source[^>]+\>/i", ' ', $tag);
433 }
434
435 return $tag;
436 }
437
438 private function replace_image($tag, $placeholder)
439 {
440 $pos = strpos($tag, 'data-src=');
441
442 if ($this->browser_lazy) {
443 $tag = str_replace('<img', '<img loading="lazy"', $tag);
444 } elseif (!$pos) {
445 $tag = str_replace(' src=', ' src=\'' . $placeholder . '\' data-src=', $tag);
446 }
447
448 return $tag;
449 }
450
451 public function add_lazyload_for_images_pagespeed($tag)
452 {
453 //this one is reverse, if image is excluded or option is disabled we add attribute
454 $two_lazyload = empty($this->TwoSettings->get_settings('two_lazyload', 'off')) ? 'off' : 'on';
455
456 if ($this->isImage($tag) && ($this->isExcluded($tag, 'two_exclude_lazyload') || $two_lazyload === 'off')) {
457 return str_replace(' src=', ' ' . esc_attr(OptimizerScripts::TWO_DISABLE_PAGESPEED_DEFER_ATTRIBUTE) . ' src=', $tag);
458 }
459
460 return $tag;
461 }
462
463 public function disable_optimisation_for_images_pagespeed($tag)
464 {
465 //this one is reverse, if image is excluded or option is disabled we add attribute
466 $two_do_not_optimize_images = empty($this->TwoSettings->get_settings('two_do_not_optimize_images', 'off')) ? 'off' : 'on';
467
468 if ($this->isImage($tag) && ($this->isExcluded($tag, 'two_exclude_images_for_optimize') || $two_do_not_optimize_images === 'on')) {
469 return str_replace(' src=', ' data-pagespeed-no-transform src=', $tag);
470 }
471
472 return $tag;
473 }
474
475 private function isImage($tag)
476 {
477 return strpos($tag, '<img') !== false;
478 }
479
480 private function isIframe($tag)
481 {
482 return strpos($tag, '<iframe') !== false;
483 }
484
485 private function isVideo($tag)
486 {
487 return strpos($tag, '<video') !== false;
488 }
489
490 public function image_lazyload($tag)
491 {
492 if (OptimizerUtils::is_pagespeed_lazyload_enabled()) {
493 return $this->add_lazyload_for_images_pagespeed($tag);
494 }
495
496 return $this->add_lazyload($tag);
497 }
498
499 public function disable_pagespeed_image_optimization($tag)
500 {
501 if (OptimizerUtils::is_pagespeed_image_optimization_enables()) {
502 return $this->disable_optimisation_for_images_pagespeed($tag);
503 }
504
505 return $tag;
506 }
507
508 public function isExcluded($tag, $option_name)
509 {
510 /*
511 * keywords for popular sliders
512 * exclude slider images
513 * */
514
515 $slider_keywords = [
516 'soliloquy',
517 ];
518
519 foreach ($slider_keywords as $val) {
520 if (strpos($tag, $val) !== false) {
521 return true;
522 }
523 }
524 preg_match('@' . self::TWO_SRC_ATTRIBUTE_VALUE_REGEXP . '@', $tag, $match);
525 $src = array_pop($match);
526
527 if (!isset($src) || $src === '') {
528 return true;
529 }
530 $two_exclude_tag = $this->TwoSettings->get_settings($option_name);
531 global $TwoSettings;
532 $two_img_in_viewport_lazyload = $TwoSettings->get_settings('two_img_in_viewport_lazyload');
533
534 if ($two_img_in_viewport_lazyload == 'on') {
535 $critical = new OptimizerCriticalCss();
536
537 if (!empty($critical->images_in_viewport) && is_array($critical->images_in_viewport)) {
538 $two_exclude_tag .= ',' . implode(',', $critical->images_in_viewport);
539 }
540 }
541
542 if (isset($two_exclude_tag) && !empty($two_exclude_tag)) {
543 $exclude_tag = explode(',', $two_exclude_tag);
544
545 foreach ($exclude_tag as $name) {
546 if (!empty($name) && strpos($src, $name) !== false) {
547 return true;
548 }
549 }
550 }
551
552 return false;
553 }
554
555 public function isExcludedTag($tag)
556 {
557 foreach (self::TWO_EXCLUDE_LAZY_KEYWORDS as $val) {
558 if (strpos($tag, $val) !== false) {
559 return true;
560 }
561 }
562
563 return false;
564 }
565
566 public function get_lazyload_exclusions()
567 {
568 return [];
569 }
570
571 public static function inject_classes_in_tag($tag, $target_class)
572 {
573 if (strpos($tag, 'class=') !== false) {
574 $tag = preg_replace('/(\sclass\s?=\s?("|\'))/', '$1 ' . $target_class . ' ', $tag);
575 } else {
576 if (strpos($tag, '<img') !== false) {
577 $tag = str_replace('<img', '<img class="' . trim($target_class) . '" ', $tag);
578 } elseif (strpos($tag, '<iframe') !== false) {
579 global $TwoSettings;
580 $two_delay_iframe_lazyload = $TwoSettings->get_settings('two_delay_iframe_lazyload');
581
582 if ($two_delay_iframe_lazyload == 'on') {
583 $tag = str_replace('<iframe', '<iframe class="' . trim($target_class) . '_delay" ', $tag);
584 } else {
585 $tag = str_replace('<iframe', '<iframe class="' . trim($target_class) . '" ', $tag);
586 }
587 } elseif (strpos($tag, '<video') !== false) {
588 $tag = str_replace('<video', '<video class="' . trim($target_class) . '" ', $tag);
589 }
590 }
591
592 return $tag;
593 }
594
595 public function get_default_lazyload_placeholder($imgopt_w, $imgopt_h)
596 {
597 return OptimizerUtils::SVG_DATA . $imgopt_w . '%20' . $imgopt_h . '%22%3E%3C/svg%3E';
598 }
599
600 public function get_smart_lazy_load_data()
601 {
602 return $this->smart_lazy_load_data;
603 }
604
605 private function smart_lazy_load_data($flag)
606 {
607 $this->smart_lazy_load_data[$flag] = true;
608 }
609
610 /**
611 * Add missing attachment id-s to img tags and run WP function to add srcset and sizes attributes
612 *
613 * @param $content HTML content of the page
614 *
615 * @return string html page
616 *
617 * @throws \DiDom\Exceptions\InvalidSelectorException
618 */
619 public static function add_attachment_id_to_img($content)
620 {
621 $document = new \DiDom\Document($content);
622 $images = $document->find('img:not([srcset]):not([class*="wp-image-"])');
623 $image_urls = [];
624 $dir = wp_get_upload_dir();
625
626 // search all images in database with one query
627 foreach ($images as $i => $image) {
628 $imgUrl = $image->attr('src');
629 $url = isset($imgUrl) ? $image->attr('src') : $image->attr('data-src');
630
631 if ($url && 0 === strpos($url, $dir['baseurl'])) {
632 $url = substr($url, strlen($dir[ 'baseurl' ] . '/'));
633 // If the URL is auto-generated thumbnail, remove the sizes and get the URL of the original image
634 $url = preg_replace('/-\d+x\d+(?=\.(jpg|jpeg|png|gif)$)/i', '', $url);
635 $image_urls[ $i ] = $url;
636 }
637 }
638 global $wpdb;
639 $sql = $wpdb->prepare(
640 "SELECT meta_value, post_id FROM $wpdb->postmeta WHERE meta_key = '_wp_attached_file' AND meta_value IN (%s)",
641 implode(',', $image_urls)
642 );
643 $attach_ids = $wpdb->get_results($sql, OBJECT_K); // phpcs:ignore
644
645 // add id to img-s w/o id class
646 foreach ($images as $i => $image) {
647 $imgUrl = $image->attr('src');
648 $url = isset($imgUrl) ? $image->attr('src') : $image->attr('data-src');
649
650 if ($url && 0 === strpos($url, $dir['baseurl'])) {
651 $url_original = substr($url, strlen($dir[ 'baseurl' ] . '/'));
652 // If the URL is auto-generated thumbnail, remove the sizes and get the URL of the original image
653 $url = preg_replace('/-\d+x\d+(?=\.(jpg|jpeg|png|gif)$)/i', '', $url_original);
654
655 if (isset($attach_ids[ $url ])) {
656 $image->attr('class', 'wp-image-' . $attach_ids[ $url ]->post_id);
657
658 if (preg_match_all('#<img[^>]*src=[^>]*' . $url_original . '[^>]*>#Usmi', $content, $matches)) {
659 foreach ($matches[0] as $tag) {
660 $to_replace[ $tag ] = self::inject_classes_in_tag($tag, 'wp-image-' . $attach_ids[ $url ]->post_id);
661 }
662 $content = str_replace(array_keys($to_replace), array_values($to_replace), $content);
663 }
664 }
665 }
666 }
667
668 // return html with added srcset and sizes attributes
669 if (function_exists('wp_filter_content_tags')) {
670 // Since WP 5.5.0
671 return wp_filter_content_tags($content);
672 } elseif (function_exists('wp_make_content_images_responsive')) {
673 // Since WP 4.4.0. Deprecated in WP 5.5.0
674 return wp_make_content_images_responsive($content);
675 }
676 }
677
678 /**
679 * Replace img tags with picture tags.
680 * Thanks to Grégory Viguier for inspiration and some code fragments.
681 *
682 * @param $content HTML content of the page
683 *
684 * @return string html page
685 */
686 public static function replace_img_with_picture($content)
687 {
688 $html_no_picture_tags = self::remove_picture_tags($content);
689 $images = self::get_images($html_no_picture_tags);
690
691 if (! $images) {
692 return $content;
693 }
694
695 foreach ($images as $image) {
696 $tag = self::build_picture_tag($image);
697 $content = str_replace($image['tag'], $tag, $content);
698 }
699
700 return $content;
701 }
702
703 private static function remove_picture_tags($html)
704 {
705 $replace = preg_replace('#<picture[^>]*>.*?<\/picture\s*>#mis', '', $html);
706
707 if (null === $replace) {
708 return $html;
709 }
710
711 return $replace;
712 }
713
714 protected static function get_images($content)
715 {
716 // Remove comments.
717 $content = preg_replace('/<!--(.*)-->/Uis', '', $content);
718
719 if (! preg_match_all('/<img\s.*>/isU', $content, $matches)) {
720 return [];
721 }
722
723 $images = array_map([ 'self', 'process_image' ], $matches[0]);
724 $images = array_filter($images);
725
726 if (! $images || ! is_array($images)) {
727 return [];
728 }
729
730 foreach ($images as $i => $image) {
731 if (empty($image['src']['webp_exists']) || empty($image['src']['webp_url'])) {
732 unset($images[ $i ]);
733 continue;
734 }
735
736 unset($images[ $i ]['src']['webp_path'], $images[ $i ]['src']['webp_exists']);
737
738 if (empty($image['srcset']) || ! is_array($image['srcset'])) {
739 unset($images[ $i ]['srcset']);
740 continue;
741 }
742
743 foreach ($image['srcset'] as $j => $srcset) {
744 if (! is_array($srcset)) {
745 continue;
746 }
747
748 if (empty($srcset['webp_exists']) || empty($srcset['webp_url'])) {
749 unset($images[ $i ]['srcset'][ $j ]['webp_url']);
750 }
751
752 unset($images[ $i ]['srcset'][ $j ]['webp_path'], $images[ $i ]['srcset'][ $j ]['webp_exists']);
753 }
754 }
755
756 return $images;
757 }
758
759 protected static function process_image($image)
760 {
761 $atts_pattern = '/(?<name>[^\s"\']+)\s*=\s*(["\'])\s*(?<value>.*?)\s*\2/s';
762
763 if (! preg_match_all($atts_pattern, $image, $tmp_attributes, PREG_SET_ORDER)) {
764 // No attributes?
765 return false;
766 }
767
768 $attributes = [];
769
770 foreach ($tmp_attributes as $attribute) {
771 $attributes[ $attribute['name'] ] = $attribute['value'];
772 }
773
774 if (! empty($attributes['class']) && strpos($attributes['class'], 'two-no-webp') !== false) {
775 return false;
776 }
777
778 // Deal with the src attribute.
779 $src_source = false;
780
781 foreach ([ 'data-lazy-src', 'data-src', 'src' ] as $src_attr) {
782 if (! empty($attributes[ $src_attr ])) {
783 $src_source = $src_attr;
784 break;
785 }
786 }
787
788 if (! $src_source) {
789 // No src attribute.
790 return false;
791 }
792
793 $extensions = [
794 'jpg|jpeg|jpe' => 'image/jpeg',
795 'png' => 'image/png',
796 // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
797 //'gif' => 'image/gif',
798 ];
799 $extensions = array_keys($extensions);
800 $extensions = implode('|', $extensions);
801
802 if (! preg_match('@^(?<src>(?:(?:https?:)?//|/).+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $attributes[ $src_source ], $src)) {
803 // Not a supported image format.
804 return false;
805 }
806
807 $webp_url = $src['src'] . '.webp';
808 $webp_path = self::url_to_path($webp_url);
809 $webp_url .= ! empty($src['query']) ? $src['query'] : '';
810
811 $data = [
812 'tag' => $image,
813 'attributes' => $attributes,
814 'src_attribute' => $src_source,
815 'src' => [
816 'url' => $attributes[ $src_source ],
817 'webp_url' => $webp_url,
818 'webp_path' => $webp_path,
819 'webp_exists' => $webp_path && @file_exists($webp_path),
820 ],
821 'srcset_attribute' => false,
822 'srcset' => [],
823 ];
824
825 // Deal with the srcset attribute.
826 $srcset_source = false;
827
828 foreach ([ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr) {
829 if (! empty($attributes[ $srcset_attr ])) {
830 $srcset_source = $srcset_attr;
831 break;
832 }
833 }
834
835 if ($srcset_source) {
836 $data['srcset_attribute'] = $srcset_source;
837
838 $srcset = explode(',', $attributes[ $srcset_source ]);
839
840 foreach ($srcset as $srcs) {
841 $srcs = preg_split('/\s+/', trim($srcs));
842
843 if (count($srcs) > 2) {
844 // Not a good idea to have space characters in file name.
845 $descriptor = array_pop($srcs);
846 $srcs = [ implode(' ', $srcs), $descriptor ];
847 }
848
849 if (empty($srcs[1])) {
850 $srcs[1] = '1x';
851 }
852
853 if (! preg_match('@^(?<src>(?:https?:)?//.+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $srcs[0], $src)) {
854 // Not a supported image format.
855 $data['srcset'][] = [
856 'url' => $srcs[0],
857 'descriptor' => $srcs[1],
858 ];
859 continue;
860 }
861
862 $webp_url = $src['src'] . '.webp';
863 $webp_path = self::url_to_path($webp_url);
864 $webp_url .= ! empty($src['query']) ? $src['query'] : '';
865
866 $data['srcset'][] = [
867 'url' => $srcs[0],
868 'descriptor' => $srcs[1],
869 'webp_url' => $webp_url,
870 'webp_path' => $webp_path,
871 'webp_exists' => $webp_path && @file_exists($webp_path),
872 ];
873 }
874 }
875
876 if (! $data || ! is_array($data)) {
877 return false;
878 }
879
880 if (! isset($data['tag'], $data['attributes'], $data['src_attribute'], $data['src'], $data['srcset_attribute'], $data['srcset'])) {
881 return false;
882 }
883
884 return $data;
885 }
886
887 protected static function url_to_path($url)
888 {
889 /**
890 * $url, $uploads_url, $root_url, and $cdn_url are passed through `set_url_scheme()` only to make sure `stripos()` doesn't fail over a stupid http/https difference.
891 */
892 $uploads = wp_upload_dir();
893 $baseurl = trailingslashit($uploads['baseurl']) ?? '';
894 $uploads_url = set_url_scheme($baseurl);
895 $basedir = trailingslashit($uploads['basedir']) ?? '';
896 $uploads_dir = wp_normalize_path($basedir);
897 $root_url = set_url_scheme(home_url('/'));
898 $root_dir = ABSPATH;
899 $domain_url = wp_parse_url($root_url);
900
901 if (! empty($domain_url['scheme']) && ! empty($domain_url['host'])) {
902 $domain_url = $domain_url['scheme'] . '://' . $domain_url['host'] . '/';
903 } else {
904 $domain_url = false;
905 }
906
907 // Get the right URL format.
908 if ($domain_url && strpos($url, '/') === 0) {
909 // URL like `/path/to/image.jpg.webp`.
910 $url = $domain_url . ltrim($url, '/');
911 }
912
913 $url = set_url_scheme($url);
914
915 // Return the path.
916 if (stripos($url, $uploads_url) === 0) {
917 return str_ireplace($uploads_url, $uploads_dir, $url);
918 }
919
920 if (stripos($url, $root_url) === 0) {
921 return str_ireplace($root_url, $root_dir, $url);
922 }
923
924 return false;
925 }
926
927 protected static function build_picture_tag($image)
928 {
929 $to_remove = [
930 'alt' => '',
931 'height' => '',
932 'width' => '',
933 'data-lazy-src' => '',
934 'data-src' => '',
935 'src' => '',
936 'data-lazy-srcset' => '',
937 'data-srcset' => '',
938 'srcset' => '',
939 'data-lazy-sizes' => '',
940 'data-sizes' => '',
941 'sizes' => '',
942 ];
943
944 $attributes = array_diff_key($image['attributes'], $to_remove);
945
946 /*
947 * Remove Gutenberg specific attributes from picture tag, leave them on img tag.
948 */
949 if (! empty($image['attributes']['class']) && strpos($image['attributes']['class'], 'wp-block-cover__image-background') !== false) {
950 unset($attributes['style']);
951 unset($attributes['class']);
952 unset($attributes['data-object-fit']);
953 unset($attributes['data-object-position']);
954 }
955
956 $output = '<picture' . self::build_attributes($attributes) . ">\n";
957 $output .= self::build_source_tag($image);
958 $output .= self::build_img_tag($image);
959 $output .= "</picture>\n";
960
961 return $output;
962 }
963
964 protected static function build_attributes($attributes)
965 {
966 if (! $attributes || ! is_array($attributes)) {
967 return '';
968 }
969
970 $out = '';
971
972 foreach ($attributes as $attribute => $value) {
973 $out .= ' ' . $attribute . '="' . esc_attr($value) . '"';
974 }
975
976 return $out;
977 }
978
979 protected static function build_source_tag($image)
980 {
981 $srcset_source = ! empty($image['srcset_attribute']) ? $image['srcset_attribute'] : $image['src_attribute'] . 'set';
982 $attributes = [
983 'type' => 'image/webp',
984 $srcset_source => [],
985 ];
986
987 if (! empty($image['srcset'])) {
988 foreach ($image['srcset'] as $srcset) {
989 if (empty($srcset['webp_url'])) {
990 continue;
991 }
992
993 $attributes[ $srcset_source ][] = $srcset['webp_url'] . ' ' . $srcset['descriptor'];
994 }
995 }
996
997 if (empty($attributes[ $srcset_source ])) {
998 $attributes[ $srcset_source ][] = $image['src']['webp_url'];
999 }
1000
1001 $attributes[ $srcset_source ] = implode(', ', $attributes[ $srcset_source ]);
1002
1003 foreach ([ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr) {
1004 if (! empty($image['attributes'][ $srcset_attr ]) && $srcset_attr !== $srcset_source) {
1005 $attributes[ $srcset_attr ] = $image['attributes'][ $srcset_attr ];
1006 }
1007 }
1008
1009 if ('srcset' !== $srcset_source && empty($attributes['srcset']) && ! empty($image['attributes']['src'])) {
1010 // Lazyload: the "src" attr should contain a placeholder (a data image or a blank.gif ).
1011 $attributes['srcset'] = $image['attributes']['src'];
1012 }
1013
1014 foreach ([ 'data-lazy-sizes', 'data-sizes', 'sizes' ] as $sizes_attr) {
1015 if (! empty($image['attributes'][ $sizes_attr ])) {
1016 $attributes[ $sizes_attr ] = $image['attributes'][ $sizes_attr ];
1017 }
1018 }
1019
1020 return '<source' . self::build_attributes($attributes) . "/>\n";
1021 }
1022
1023 protected static function build_img_tag($image)
1024 {
1025 /*
1026 * Gutenberg fix.
1027 * Check for the 'wp-block-cover__image-background' class on the original image, and leave that class and style attributes if found.
1028 */
1029 if (! empty($image['attributes']['class']) && strpos($image['attributes']['class'], 'wp-block-cover__image-background') !== false) {
1030 $to_remove = [
1031 'id' => '',
1032 'title' => '',
1033 ];
1034
1035 $attributes = array_diff_key($image['attributes'], $to_remove);
1036 } else {
1037 $to_remove = [
1038 'class' => '',
1039 'id' => '',
1040 'style' => '',
1041 'title' => '',
1042 ];
1043
1044 $attributes = array_diff_key($image['attributes'], $to_remove);
1045 }
1046
1047 return '<img' . self::build_attributes($attributes) . "/>\n";
1048 }
1049 }
1050