PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 1.9.13
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v1.9.13
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / classes / Webp / Picture / Display.php

Display.php in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 1.9.13, at classes/Webp/Picture/Display.php

753 lines 19.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Imagify\Webp\Picture;
3
4 defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' );
5
6 /**
7 * Display webp images on the site with <picture> tags.
8 *
9 * @since 1.9
10 * @author Grégory Viguier
11 */
12 class Display {
13 use \Imagify\Traits\InstanceGetterTrait;
14
15 /**
16 * Option value.
17 *
18 * @var string
19 * @since 1.9
20 * @author Grégory Viguier
21 */
22 const OPTION_VALUE = 'picture';
23
24 /**
25 * Filesystem object.
26 *
27 * @var \Imagify_Filesystem
28 * @since 1.9
29 * @access protected
30 * @author Grégory Viguier
31 */
32 protected $filesystem;
33
34 /**
35 * Constructor.
36 *
37 * @since 1.9
38 * @access public
39 * @author Grégory Viguier
40 */
41 public function __construct() {
42 $this->filesystem = \Imagify_Filesystem::get_instance();
43 }
44
45 /**
46 * Init.
47 *
48 * @since 1.9
49 * @access public
50 * @author Grégory Viguier
51 */
52 public function init() {
53 add_action( 'template_redirect', [ $this, 'start_content_process' ], -1000 );
54 }
55
56 /** ----------------------------------------------------------------------------------------- */
57 /** ADD <PICTURE> TAGS TO THE PAGE ========================================================== */
58 /** ----------------------------------------------------------------------------------------- */
59
60 /**
61 * Start buffering the page content.
62 *
63 * @since 1.9
64 * @access public
65 * @author Grégory Viguier
66 */
67 public function start_content_process() {
68 if ( ! get_imagify_option( 'display_webp' ) ) {
69 return;
70 }
71
72 if ( self::OPTION_VALUE !== get_imagify_option( 'display_webp_method' ) ) {
73 return;
74 }
75
76 /**
77 * Prevent the replacement of <img> tags into <picture> tags.
78 *
79 * @since 1.9
80 * @author Grégory Viguier
81 *
82 * @param bool $allow True to allow the use of <picture> tags (default). False to prevent their use.
83 */
84 $allow = apply_filters( 'imagify_allow_picture_tags_for_webp', true );
85
86 if ( ! $allow ) {
87 return;
88 }
89
90 ob_start( [ $this, 'maybe_process_buffer' ] );
91 }
92
93 /**
94 * Maybe process the page content.
95 *
96 * @since 1.9
97 * @access public
98 * @author Grégory Viguier
99 *
100 * @param string $buffer The buffer content.
101 * @return string
102 */
103 public function maybe_process_buffer( $buffer ) {
104 if ( ! $this->is_html( $buffer ) ) {
105 return $buffer;
106 }
107
108 if ( strlen( $buffer ) <= 255 ) {
109 // Buffer length must be > 255 (IE does not read pages under 255 c).
110 return $buffer;
111 }
112
113 $buffer = $this->process_content( $buffer );
114
115 /**
116 * Filter the page content after Imagify.
117 *
118 * @since 1.9
119 * @author Grégory Viguier
120 *
121 * @param string $buffer The page content.
122 */
123 $buffer = (string) apply_filters( 'imagify_buffer', $buffer );
124
125 return $buffer;
126 }
127
128 /**
129 * Process the content.
130 *
131 * @since 1.9
132 * @access public
133 * @author Grégory Viguier
134 *
135 * @param string $content The content.
136 * @return string
137 */
138 public function process_content( $content ) {
139 $images = $this->get_images( $content );
140
141 if ( ! $images ) {
142 return $content;
143 }
144
145 foreach ( $images as $image ) {
146 $tag = $this->build_picture_tag( $image );
147 $content = str_replace( $image['tag'], $tag, $content );
148 }
149
150 return $content;
151 }
152
153 /** ----------------------------------------------------------------------------------------- */
154 /** BUILD HTML TAGS AND ATTRIBUTES ========================================================== */
155 /** ----------------------------------------------------------------------------------------- */
156
157 /**
158 * Build the <picture> tag to insert.
159 *
160 * @since 1.9
161 * @see $this->process_image()
162 * @access protected
163 * @author Grégory Viguier
164 *
165 * @param array $image An array of data.
166 * @return string A <picture> tag.
167 */
168 protected function build_picture_tag( $image ) {
169 $to_remove = [
170 'alt' => '',
171 'height' => '',
172 'width' => '',
173 'data-lazy-src' => '',
174 'data-src' => '',
175 'src' => '',
176 'data-lazy-srcset' => '',
177 'data-srcset' => '',
178 'srcset' => '',
179 'data-lazy-sizes' => '',
180 'data-sizes' => '',
181 'sizes' => '',
182 ];
183
184 $attributes = array_diff_key( $image['attributes'], $to_remove );
185
186 /**
187 * Filter the attributes to be added to the <picture> tag.
188 *
189 * @since 1.9
190 * @author Grégory Viguier
191 *
192 * @param array $attributes A list of attributes to be added to the <picture> tag.
193 * @param array $data Data built from the originale <img> tag. See $this->process_image().
194 */
195 $attributes = apply_filters( 'imagify_picture_attributes', $attributes, $image );
196
197 $output = '<picture' . $this->build_attributes( $attributes ) . ">\n";
198 /**
199 * Allow to add more <source> tags to the <picture> tag.
200 *
201 * @since 1.9
202 * @author Grégory Viguier
203 *
204 * @param string $more_source_tags Additional <source> tags.
205 * @param array $data Data built from the originale <img> tag. See $this->process_image().
206 */
207 $output .= apply_filters( 'imagify_additional_source_tags', '', $image );
208 $output .= $this->build_source_tag( $image );
209 $output .= $this->build_img_tag( $image );
210 $output .= "</picture>\n";
211
212 return $output;
213 }
214
215 /**
216 * Build the <source> tag to insert in the <picture>.
217 *
218 * @since 1.9
219 * @see $this->process_image()
220 * @access protected
221 * @author Grégory Viguier
222 *
223 * @param array $image An array of data.
224 * @return string A <source> tag.
225 */
226 protected function build_source_tag( $image ) {
227 $srcset_source = ! empty( $image['srcset_attribute'] ) ? $image['srcset_attribute'] : $image['src_attribute'] . 'set';
228 $attributes = [
229 'type' => 'image/webp',
230 $srcset_source => [],
231 ];
232
233 if ( ! empty( $image['srcset'] ) ) {
234 foreach ( $image['srcset'] as $srcset ) {
235 if ( empty( $srcset['webp_url'] ) ) {
236 continue;
237 }
238
239 $attributes[ $srcset_source ][] = $srcset['webp_url'] . ' ' . $srcset['descriptor'];
240 }
241 }
242
243 if ( empty( $attributes[ $srcset_source ] ) ) {
244 $attributes[ $srcset_source ][] = $image['src']['webp_url'];
245 }
246
247 $attributes[ $srcset_source ] = implode( ', ', $attributes[ $srcset_source ] );
248
249 foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) {
250 if ( ! empty( $image['attributes'][ $srcset_attr ] ) && $srcset_attr !== $srcset_source ) {
251 $attributes[ $srcset_attr ] = $image['attributes'][ $srcset_attr ];
252 }
253 }
254
255 if ( 'srcset' !== $srcset_source && empty( $attributes['srcset'] ) && ! empty( $image['attributes']['src'] ) ) {
256 // Lazyload: the "src" attr should contain a placeholder (a data image or a blank.gif ).
257 $attributes['srcset'] = $image['attributes']['src'];
258 }
259
260 foreach ( [ 'data-lazy-sizes', 'data-sizes', 'sizes' ] as $sizes_attr ) {
261 if ( ! empty( $image['attributes'][ $sizes_attr ] ) ) {
262 $attributes[ $sizes_attr ] = $image['attributes'][ $sizes_attr ];
263 }
264 }
265
266 /**
267 * Filter the attributes to be added to the <source> tag.
268 *
269 * @since 1.9
270 * @author Grégory Viguier
271 *
272 * @param array $attributes A list of attributes to be added to the <source> tag.
273 * @param array $data Data built from the original <img> tag. See $this->process_image().
274 */
275 $attributes = apply_filters( 'imagify_picture_source_attributes', $attributes, $image );
276
277 return '<source' . $this->build_attributes( $attributes ) . "/>\n";
278 }
279
280 /**
281 * Build the <img> tag to insert in the <picture>.
282 *
283 * @since 1.9
284 * @see $this->process_image()
285 * @access protected
286 * @author Grégory Viguier
287 *
288 * @param array $image An array of data.
289 * @return string A <img> tag.
290 */
291 protected function build_img_tag( $image ) {
292 $to_remove = [
293 'class' => '',
294 'id' => '',
295 'style' => '',
296 'title' => '',
297 ];
298
299 $attributes = array_diff_key( $image['attributes'], $to_remove );
300
301 /**
302 * Filter the attributes to be added to the <img> tag.
303 *
304 * @since 1.9
305 * @author Grégory Viguier
306 *
307 * @param array $attributes A list of attributes to be added to the <img> tag.
308 * @param array $data Data built from the originale <img> tag. See $this->process_image().
309 */
310 $attributes = apply_filters( 'imagify_picture_img_attributes', $attributes, $image );
311
312 return '<img' . $this->build_attributes( $attributes ) . "/>\n";
313 }
314
315 /**
316 * Create HTML attributes from an array.
317 *
318 * @since 1.9
319 * @access protected
320 * @author Grégory Viguier
321 *
322 * @param array $attributes A list of attribute pairs.
323 * @return string HTML attributes.
324 */
325 protected function build_attributes( $attributes ) {
326 if ( ! $attributes || ! is_array( $attributes ) ) {
327 return '';
328 }
329
330 $out = '';
331
332 foreach ( $attributes as $attribute => $value ) {
333 $out .= ' ' . $attribute . '="' . esc_attr( $value ) . '"';
334 }
335
336 return $out;
337 }
338
339 /** ----------------------------------------------------------------------------------------- */
340 /** VARIOUS TOOLS =========================================================================== */
341 /** ----------------------------------------------------------------------------------------- */
342
343 /**
344 * Get a list of images in a content.
345 *
346 * @since 1.9
347 * @access protected
348 * @author Grégory Viguier
349 *
350 * @param string $content The content.
351 * @return array
352 */
353 protected function get_images( $content ) {
354 // Remove comments.
355 $content = preg_replace( '/<!--(.*)-->/Uis', '', $content );
356
357 if ( ! preg_match_all( '/<img\s.*>/isU', $content, $matches ) ) {
358 return [];
359 }
360
361 $images = array_map( [ $this, 'process_image' ], $matches[0] );
362 $images = array_filter( $images );
363
364 /**
365 * Filter the images to display with a <picture> tag.
366 *
367 * @since 1.9
368 * @see $this->process_image()
369 * @author Grégory Viguier
370 *
371 * @param array $images A list of arrays.
372 * @param string $content The page content.
373 */
374 $images = apply_filters( 'imagify_webp_picture_images_to_display', $images, $content );
375
376 if ( ! $images || ! is_array( $images ) ) {
377 return [];
378 }
379
380 foreach ( $images as $i => $image ) {
381 if ( empty( $image['src']['webp_exists'] ) || empty( $image['src']['webp_url'] ) ) {
382 unset( $images[ $i ] );
383 continue;
384 }
385
386 unset( $images[ $i ]['src']['webp_path'], $images[ $i ]['src']['webp_exists'] );
387
388 if ( empty( $image['srcset'] ) || ! is_array( $image['srcset'] ) ) {
389 unset( $images[ $i ]['srcset'] );
390 continue;
391 }
392
393 foreach ( $image['srcset'] as $j => $srcset ) {
394 if ( ! is_array( $srcset ) ) {
395 continue;
396 }
397
398 if ( empty( $srcset['webp_exists'] ) || empty( $srcset['webp_url'] ) ) {
399 unset( $images[ $i ]['srcset'][ $j ]['webp_url'] );
400 }
401
402 unset( $images[ $i ]['srcset'][ $j ]['webp_path'], $images[ $i ]['srcset'][ $j ]['webp_exists'] );
403 }
404 }
405
406 return $images;
407 }
408
409 /**
410 * Process an image tag and get an array containing some data.
411 *
412 * @since 1.9
413 * @access protected
414 * @author Grégory Viguier
415 *
416 * @param string $image An image html tag.
417 * @return array|false {
418 * An array of data if the image has a webp version. False otherwise.
419 *
420 * @type string $tag The image tag.
421 * @type array $attributes The image attributes (minus src and srcset).
422 * @type array $src {
423 * @type string $url URL to the original image.
424 * @type string $webp_url URL to the webp version.
425 * }
426 * @type array $srcset {
427 * An array or arrays. Not set if not applicable.
428 *
429 * @type string $url URL to the original image.
430 * @type string $webp_url URL to the webp version. Not set if not applicable.
431 * @type string $descriptor A src descriptor.
432 * }
433 * }
434 */
435 protected function process_image( $image ) {
436 static $extensions;
437
438 $atts_pattern = '/(?<name>[^\s"\']+)\s*=\s*(["\'])\s*(?<value>.*?)\s*\2/s';
439
440 if ( ! preg_match_all( $atts_pattern, $image, $tmp_attributes, PREG_SET_ORDER ) ) {
441 // No attributes?
442 return false;
443 }
444
445 $attributes = [];
446
447 foreach ( $tmp_attributes as $attribute ) {
448 $attributes[ $attribute['name'] ] = $attribute['value'];
449 }
450
451 if ( ! empty( $attributes['class'] ) && strpos( $attributes['class'], 'imagify-no-webp' ) !== false ) {
452 // Has the 'imagify-no-webp' class.
453 return false;
454 }
455
456 // Deal with the src attribute.
457 $src_source = false;
458
459 foreach ( [ 'data-lazy-src', 'data-src', 'src' ] as $src_attr ) {
460 if ( ! empty( $attributes[ $src_attr ] ) ) {
461 $src_source = $src_attr;
462 break;
463 }
464 }
465
466 if ( ! $src_source ) {
467 // No src attribute.
468 return false;
469 }
470
471 if ( ! isset( $extensions ) ) {
472 $extensions = imagify_get_mime_types( 'image' );
473 $extensions = array_keys( $extensions );
474 $extensions = implode( '|', $extensions );
475 }
476
477 if ( ! preg_match( '@^(?<src>(?:(?:https?:)?//|/).+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $attributes[ $src_source ], $src ) ) {
478 // Not a supported image format.
479 return false;
480 }
481
482 $webp_url = imagify_path_to_webp( $src['src'] );
483 $webp_path = $this->url_to_path( $webp_url );
484 $webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
485
486 $data = [
487 'tag' => $image,
488 'attributes' => $attributes,
489 'src_attribute' => $src_source,
490 'src' => [
491 'url' => $attributes[ $src_source ],
492 'webp_url' => $webp_url,
493 'webp_path' => $webp_path,
494 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ),
495 ],
496 'srcset_attribute' => false,
497 'srcset' => [],
498 ];
499
500 // Deal with the srcset attribute.
501 $srcset_source = false;
502
503 foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) {
504 if ( ! empty( $attributes[ $srcset_attr ] ) ) {
505 $srcset_source = $srcset_attr;
506 break;
507 }
508 }
509
510 if ( $srcset_source ) {
511 $data['srcset_attribute'] = $srcset_source;
512
513 $srcset = explode( ',', $attributes[ $srcset_source ] );
514
515 foreach ( $srcset as $srcs ) {
516 $srcs = preg_split( '/\s+/', trim( $srcs ) );
517
518 if ( count( $srcs ) > 2 ) {
519 // Not a good idea to have space characters in file name.
520 $descriptor = array_pop( $srcs );
521 $srcs = [ implode( ' ', $srcs ), $descriptor ];
522 }
523
524 if ( empty( $srcs[1] ) ) {
525 $srcs[1] = '1x';
526 }
527
528 if ( ! preg_match( '@^(?<src>(?:https?:)?//.+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $srcs[0], $src ) ) {
529 // Not a supported image format.
530 $data['srcset'][] = [
531 'url' => $srcs[0],
532 'descriptor' => $srcs[1],
533 ];
534 continue;
535 }
536
537 $webp_url = imagify_path_to_webp( $src['src'] );
538 $webp_path = $this->url_to_path( $webp_url );
539 $webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
540
541 $data['srcset'][] = [
542 'url' => $srcs[0],
543 'descriptor' => $srcs[1],
544 'webp_url' => $webp_url,
545 'webp_path' => $webp_path,
546 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ),
547 ];
548 }
549 }
550
551 /**
552 * Filter a processed image tag.
553 *
554 * @since 1.9
555 * @author Grégory Viguier
556 *
557 * @param array $data An array of data for this image.
558 * @param string $image An image html tag.
559 */
560 $data = apply_filters( 'imagify_webp_picture_process_image', $data, $image );
561
562 if ( ! $data || ! is_array( $data ) ) {
563 return false;
564 }
565
566 if ( ! isset( $data['tag'], $data['attributes'], $data['src_attribute'], $data['src'], $data['srcset_attribute'], $data['srcset'] ) ) {
567 return false;
568 }
569
570 return $data;
571 }
572
573 /**
574 * Tell if a content is HTML.
575 *
576 * @since 1.9
577 * @access protected
578 * @author Grégory Viguier
579 *
580 * @param string $content The content.
581 * @return bool
582 */
583 protected function is_html( $content ) {
584 return preg_match( '/<\/html>/i', $content );
585 }
586
587 /**
588 * Convert a file URL to an absolute path.
589 *
590 * @since 1.9
591 * @access protected
592 * @author Grégory Viguier
593 *
594 * @param string $url A file URL.
595 * @return string|bool The file path. False on failure.
596 */
597 protected function url_to_path( $url ) {
598 static $uploads_url;
599 static $uploads_dir;
600 static $root_url;
601 static $root_dir;
602 static $cdn_url;
603 static $domain_url;
604
605 /**
606 * $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.
607 */
608 if ( ! isset( $uploads_url ) ) {
609 $uploads_url = set_url_scheme( $this->filesystem->get_upload_baseurl() );
610 $uploads_dir = $this->filesystem->get_upload_basedir( true );
611 $root_url = set_url_scheme( $this->filesystem->get_site_root_url() );
612 $root_dir = $this->filesystem->get_site_root();
613 $cdn_url = $this->get_cdn_source();
614 $cdn_url = $cdn_url['url'] ? set_url_scheme( $cdn_url['url'] ) : false;
615 $domain_url = wp_parse_url( $root_url );
616
617 if ( ! empty( $domain_url['scheme'] ) && ! empty( $domain_url['host'] ) ) {
618 $domain_url = $domain_url['scheme'] . '://' . $domain_url['host'] . '/';
619 } else {
620 $domain_url = false;
621 }
622 }
623
624 // Get the right URL format.
625 if ( $domain_url && strpos( $url, '/' ) === 0 ) {
626 // URL like `/path/to/image.jpg.webp`.
627 $url = $domain_url . ltrim( $url, '/' );
628 }
629
630 $url = set_url_scheme( $url );
631
632 if ( $cdn_url && $domain_url && stripos( $url, $cdn_url ) === 0 ) {
633 // CDN.
634 $url = str_ireplace( $cdn_url, $domain_url, $url );
635 }
636
637 // Return the path.
638 if ( stripos( $url, $uploads_url ) === 0 ) {
639 return str_ireplace( $uploads_url, $uploads_dir, $url );
640 }
641
642 if ( stripos( $url, $root_url ) === 0 ) {
643 return str_ireplace( $root_url, $root_dir, $url );
644 }
645
646 return false;
647 }
648
649 /**
650 * Get the CDN "source".
651 *
652 * @since 1.9.3
653 * @access public
654 * @author Grégory Viguier
655 *
656 * @param string $option_url An URL to use instead of the one stored in the option. It is used only if no constant/filter.
657 * @return array {
658 * @type string $source Where does it come from? Possible values are 'constant', 'filter', or 'option'.
659 * @type string $name Who? Can be a constant name, a plugin name, or an empty string.
660 * @type string $url The CDN URL, with a trailing slash. An empty string if no URL is set.
661 * }
662 */
663 public function get_cdn_source( $option_url = '' ) {
664 if ( defined( 'IMAGIFY_CDN_URL' ) && IMAGIFY_CDN_URL && is_string( IMAGIFY_CDN_URL ) ) {
665 // Use a constant.
666 $source = [
667 'source' => 'constant',
668 'name' => 'IMAGIFY_CDN_URL',
669 'url' => IMAGIFY_CDN_URL,
670 ];
671 } else {
672 // Maybe use a filter.
673 $filter_source = [
674 'name' => null,
675 'url' => null,
676 ];
677
678 /**
679 * Provide a custom CDN source.
680 *
681 * @since 1.9.3
682 * @author Grégory Viguier
683 *
684 * @param array $filter_source {
685 * @type $name string The name of which provides the URL (plugin name, etc).
686 * @type $url string The CDN URL.
687 * }
688 */
689 $filter_source = apply_filters( 'imagify_cdn_source', $filter_source );
690
691 if ( ! empty( $filter_source['url'] ) ) {
692 $source = [
693 'source' => 'filter',
694 'name' => ! empty( $filter_source['name'] ) ? $filter_source['name'] : '',
695 'url' => $filter_source['url'],
696 ];
697 }
698 }
699
700 if ( empty( $source['url'] ) ) {
701 // No constant, no filter: use the option.
702 $source = [
703 'source' => 'option',
704 'name' => '',
705 'url' => $option_url && is_string( $option_url ) ? $option_url : get_imagify_option( 'cdn_url' ),
706 ];
707 }
708
709 if ( empty( $source['url'] ) ) {
710 // Nothing set.
711 return [
712 'source' => 'option',
713 'name' => '',
714 'url' => '',
715 ];
716 }
717
718 $source['url'] = $this->sanitize_cdn_url( $source['url'] );
719
720 if ( empty( $source['url'] ) ) {
721 // Not an URL.
722 return [
723 'source' => 'option',
724 'name' => '',
725 'url' => '',
726 ];
727 }
728
729 return $source;
730 }
731
732 /**
733 * Sanitize the CDN URL value.
734 *
735 * @since 1.9.3
736 * @access public
737 * @author Grégory Viguier
738 *
739 * @param string $url The URL to sanitize.
740 * @return string
741 */
742 public function sanitize_cdn_url( $url ) {
743 $url = sanitize_text_field( $url );
744
745 if ( ! $url || ! preg_match( '@^https?://.+\.[^.]+@i', $url ) ) {
746 // Not an URL.
747 return '';
748 }
749
750 return trailingslashit( $url );
751 }
752 }
753