PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 1.9.8
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v1.9.8
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.8, at classes/Webp/Picture/Display.php

749 lines 19.6 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 $attributes[ $srcset_source ] = implode( ', ', $attributes[ $srcset_source ] );
244
245 foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) {
246 if ( ! empty( $image['attributes'][ $srcset_attr ] ) && $srcset_attr !== $srcset_source ) {
247 $attributes[ $srcset_attr ] = $image['attributes'][ $srcset_attr ];
248 }
249 }
250
251 if ( 'srcset' !== $srcset_source && empty( $attributes['srcset'] ) && ! empty( $image['attributes']['src'] ) ) {
252 // Lazyload: the "src" attr should contain a placeholder (a data image or a blank.gif ).
253 $attributes['srcset'] = $image['attributes']['src'];
254 }
255
256 foreach ( [ 'data-lazy-sizes', 'data-sizes', 'sizes' ] as $sizes_attr ) {
257 if ( ! empty( $image['attributes'][ $sizes_attr ] ) ) {
258 $attributes[ $sizes_attr ] = $image['attributes'][ $sizes_attr ];
259 }
260 }
261
262 /**
263 * Filter the attributes to be added to the <source> tag.
264 *
265 * @since 1.9
266 * @author Grégory Viguier
267 *
268 * @param array $attributes A list of attributes to be added to the <source> tag.
269 * @param array $data Data built from the original <img> tag. See $this->process_image().
270 */
271 $attributes = apply_filters( 'imagify_picture_source_attributes', $attributes, $image );
272
273 return '<source' . $this->build_attributes( $attributes ) . "/>\n";
274 }
275
276 /**
277 * Build the <img> tag to insert in the <picture>.
278 *
279 * @since 1.9
280 * @see $this->process_image()
281 * @access protected
282 * @author Grégory Viguier
283 *
284 * @param array $image An array of data.
285 * @return string A <img> tag.
286 */
287 protected function build_img_tag( $image ) {
288 $to_remove = [
289 'class' => '',
290 'id' => '',
291 'style' => '',
292 'title' => '',
293 ];
294
295 $attributes = array_diff_key( $image['attributes'], $to_remove );
296
297 /**
298 * Filter the attributes to be added to the <img> tag.
299 *
300 * @since 1.9
301 * @author Grégory Viguier
302 *
303 * @param array $attributes A list of attributes to be added to the <img> tag.
304 * @param array $data Data built from the originale <img> tag. See $this->process_image().
305 */
306 $attributes = apply_filters( 'imagify_picture_img_attributes', $attributes, $image );
307
308 return '<img' . $this->build_attributes( $attributes ) . "/>\n";
309 }
310
311 /**
312 * Create HTML attributes from an array.
313 *
314 * @since 1.9
315 * @access protected
316 * @author Grégory Viguier
317 *
318 * @param array $attributes A list of attribute pairs.
319 * @return string HTML attributes.
320 */
321 protected function build_attributes( $attributes ) {
322 if ( ! $attributes || ! is_array( $attributes ) ) {
323 return '';
324 }
325
326 $out = '';
327
328 foreach ( $attributes as $attribute => $value ) {
329 $out .= ' ' . $attribute . '="' . esc_attr( $value ) . '"';
330 }
331
332 return $out;
333 }
334
335 /** ----------------------------------------------------------------------------------------- */
336 /** VARIOUS TOOLS =========================================================================== */
337 /** ----------------------------------------------------------------------------------------- */
338
339 /**
340 * Get a list of images in a content.
341 *
342 * @since 1.9
343 * @access protected
344 * @author Grégory Viguier
345 *
346 * @param string $content The content.
347 * @return array
348 */
349 protected function get_images( $content ) {
350 // Remove comments.
351 $content = preg_replace( '/<!--(.*)-->/Uis', '', $content );
352
353 if ( ! preg_match_all( '/<img\s.*>/isU', $content, $matches ) ) {
354 return [];
355 }
356
357 $images = array_map( [ $this, 'process_image' ], $matches[0] );
358 $images = array_filter( $images );
359
360 /**
361 * Filter the images to display with a <picture> tag.
362 *
363 * @since 1.9
364 * @see $this->process_image()
365 * @author Grégory Viguier
366 *
367 * @param array $images A list of arrays.
368 * @param string $content The page content.
369 */
370 $images = apply_filters( 'imagify_webp_picture_images_to_display', $images, $content );
371
372 if ( ! $images || ! is_array( $images ) ) {
373 return [];
374 }
375
376 foreach ( $images as $i => $image ) {
377 if ( empty( $image['src']['webp_exists'] ) || empty( $image['src']['webp_url'] ) ) {
378 unset( $images[ $i ] );
379 continue;
380 }
381
382 unset( $images[ $i ]['src']['webp_path'], $images[ $i ]['src']['webp_exists'] );
383
384 if ( empty( $image['srcset'] ) || ! is_array( $image['srcset'] ) ) {
385 unset( $images[ $i ]['srcset'] );
386 continue;
387 }
388
389 foreach ( $image['srcset'] as $j => $srcset ) {
390 if ( ! is_array( $srcset ) ) {
391 continue;
392 }
393
394 if ( empty( $srcset['webp_exists'] ) || empty( $srcset['webp_url'] ) ) {
395 unset( $images[ $i ]['srcset'][ $j ]['webp_url'] );
396 }
397
398 unset( $images[ $i ]['srcset'][ $j ]['webp_path'], $images[ $i ]['srcset'][ $j ]['webp_exists'] );
399 }
400 }
401
402 return $images;
403 }
404
405 /**
406 * Process an image tag and get an array containing some data.
407 *
408 * @since 1.9
409 * @access protected
410 * @author Grégory Viguier
411 *
412 * @param string $image An image html tag.
413 * @return array|false {
414 * An array of data if the image has a webp version. False otherwise.
415 *
416 * @type string $tag The image tag.
417 * @type array $attributes The image attributes (minus src and srcset).
418 * @type array $src {
419 * @type string $url URL to the original image.
420 * @type string $webp_url URL to the webp version.
421 * }
422 * @type array $srcset {
423 * An array or arrays. Not set if not applicable.
424 *
425 * @type string $url URL to the original image.
426 * @type string $webp_url URL to the webp version. Not set if not applicable.
427 * @type string $descriptor A src descriptor.
428 * }
429 * }
430 */
431 protected function process_image( $image ) {
432 static $extensions;
433
434 $atts_pattern = '/(?<name>[^\s"\']+)\s*=\s*(["\'])\s*(?<value>.*?)\s*\2/s';
435
436 if ( ! preg_match_all( $atts_pattern, $image, $tmp_attributes, PREG_SET_ORDER ) ) {
437 // No attributes?
438 return false;
439 }
440
441 $attributes = [];
442
443 foreach ( $tmp_attributes as $attribute ) {
444 $attributes[ $attribute['name'] ] = $attribute['value'];
445 }
446
447 if ( ! empty( $attributes['class'] ) && strpos( $attributes['class'], 'imagify-no-webp' ) !== false ) {
448 // Has the 'imagify-no-webp' class.
449 return false;
450 }
451
452 // Deal with the src attribute.
453 $src_source = false;
454
455 foreach ( [ 'data-lazy-src', 'data-src', 'src' ] as $src_attr ) {
456 if ( ! empty( $attributes[ $src_attr ] ) ) {
457 $src_source = $src_attr;
458 break;
459 }
460 }
461
462 if ( ! $src_source ) {
463 // No src attribute.
464 return false;
465 }
466
467 if ( ! isset( $extensions ) ) {
468 $extensions = imagify_get_mime_types( 'image' );
469 $extensions = array_keys( $extensions );
470 $extensions = implode( '|', $extensions );
471 }
472
473 if ( ! preg_match( '@^(?<src>(?:(?:https?:)?//|/).+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $attributes[ $src_source ], $src ) ) {
474 // Not a supported image format.
475 return false;
476 }
477
478 $webp_url = imagify_path_to_webp( $src['src'] );
479 $webp_path = $this->url_to_path( $webp_url );
480 $webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
481
482 $data = [
483 'tag' => $image,
484 'attributes' => $attributes,
485 'src_attribute' => $src_source,
486 'src' => [
487 'url' => $attributes[ $src_source ],
488 'webp_url' => $webp_url,
489 'webp_path' => $webp_path,
490 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ),
491 ],
492 'srcset_attribute' => false,
493 'srcset' => [],
494 ];
495
496 // Deal with the srcset attribute.
497 $srcset_source = false;
498
499 foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) {
500 if ( ! empty( $attributes[ $srcset_attr ] ) ) {
501 $srcset_source = $srcset_attr;
502 break;
503 }
504 }
505
506 if ( $srcset_source ) {
507 $data['srcset_attribute'] = $srcset_source;
508
509 $srcset = explode( ',', $attributes[ $srcset_source ] );
510
511 foreach ( $srcset as $srcs ) {
512 $srcs = preg_split( '/\s+/', trim( $srcs ) );
513
514 if ( count( $srcs ) > 2 ) {
515 // Not a good idea to have space characters in file name.
516 $descriptor = array_pop( $srcs );
517 $srcs = [ implode( ' ', $srcs ), $descriptor ];
518 }
519
520 if ( empty( $srcs[1] ) ) {
521 $srcs[1] = '1x';
522 }
523
524 if ( ! preg_match( '@^(?<src>(?:https?:)?//.+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $srcs[0], $src ) ) {
525 // Not a supported image format.
526 $data['srcset'][] = [
527 'url' => $srcs[0],
528 'descriptor' => $srcs[1],
529 ];
530 continue;
531 }
532
533 $webp_url = imagify_path_to_webp( $src['src'] );
534 $webp_path = $this->url_to_path( $webp_url );
535 $webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
536
537 $data['srcset'][] = [
538 'url' => $srcs[0],
539 'descriptor' => $srcs[1],
540 'webp_url' => $webp_url,
541 'webp_path' => $webp_path,
542 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ),
543 ];
544 }
545 }
546
547 /**
548 * Filter a processed image tag.
549 *
550 * @since 1.9
551 * @author Grégory Viguier
552 *
553 * @param array $data An array of data for this image.
554 * @param string $image An image html tag.
555 */
556 $data = apply_filters( 'imagify_webp_picture_process_image', $data, $image );
557
558 if ( ! $data || ! is_array( $data ) ) {
559 return false;
560 }
561
562 if ( ! isset( $data['tag'], $data['attributes'], $data['src_attribute'], $data['src'], $data['srcset_attribute'], $data['srcset'] ) ) {
563 return false;
564 }
565
566 return $data;
567 }
568
569 /**
570 * Tell if a content is HTML.
571 *
572 * @since 1.9
573 * @access protected
574 * @author Grégory Viguier
575 *
576 * @param string $content The content.
577 * @return bool
578 */
579 protected function is_html( $content ) {
580 return preg_match( '/<\/html>/i', $content );
581 }
582
583 /**
584 * Convert a file URL to an absolute path.
585 *
586 * @since 1.9
587 * @access protected
588 * @author Grégory Viguier
589 *
590 * @param string $url A file URL.
591 * @return string|bool The file path. False on failure.
592 */
593 protected function url_to_path( $url ) {
594 static $uploads_url;
595 static $uploads_dir;
596 static $root_url;
597 static $root_dir;
598 static $cdn_url;
599 static $domain_url;
600
601 /**
602 * $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.
603 */
604 if ( ! isset( $uploads_url ) ) {
605 $uploads_url = set_url_scheme( $this->filesystem->get_upload_baseurl() );
606 $uploads_dir = $this->filesystem->get_upload_basedir( true );
607 $root_url = set_url_scheme( $this->filesystem->get_site_root_url() );
608 $root_dir = $this->filesystem->get_site_root();
609 $cdn_url = $this->get_cdn_source();
610 $cdn_url = $cdn_url['url'] ? set_url_scheme( $cdn_url['url'] ) : false;
611 $domain_url = wp_parse_url( $root_url );
612
613 if ( ! empty( $domain_url['scheme'] ) && ! empty( $domain_url['host'] ) ) {
614 $domain_url = $domain_url['scheme'] . '://' . $domain_url['host'] . '/';
615 } else {
616 $domain_url = false;
617 }
618 }
619
620 // Get the right URL format.
621 if ( $domain_url && strpos( $url, '/' ) === 0 ) {
622 // URL like `/path/to/image.jpg.webp`.
623 $url = $domain_url . ltrim( $url, '/' );
624 }
625
626 $url = set_url_scheme( $url );
627
628 if ( $cdn_url && $domain_url && stripos( $url, $cdn_url ) === 0 ) {
629 // CDN.
630 $url = str_ireplace( $cdn_url, $domain_url, $url );
631 }
632
633 // Return the path.
634 if ( stripos( $url, $uploads_url ) === 0 ) {
635 return str_ireplace( $uploads_url, $uploads_dir, $url );
636 }
637
638 if ( stripos( $url, $root_url ) === 0 ) {
639 return str_ireplace( $root_url, $root_dir, $url );
640 }
641
642 return false;
643 }
644
645 /**
646 * Get the CDN "source".
647 *
648 * @since 1.9.3
649 * @access public
650 * @author Grégory Viguier
651 *
652 * @param string $option_url An URL to use instead of the one stored in the option. It is used only if no constant/filter.
653 * @return array {
654 * @type string $source Where does it come from? Possible values are 'constant', 'filter', or 'option'.
655 * @type string $name Who? Can be a constant name, a plugin name, or an empty string.
656 * @type string $url The CDN URL, with a trailing slash. An empty string if no URL is set.
657 * }
658 */
659 public function get_cdn_source( $option_url = '' ) {
660 if ( defined( 'IMAGIFY_CDN_URL' ) && IMAGIFY_CDN_URL && is_string( IMAGIFY_CDN_URL ) ) {
661 // Use a constant.
662 $source = [
663 'source' => 'constant',
664 'name' => 'IMAGIFY_CDN_URL',
665 'url' => IMAGIFY_CDN_URL,
666 ];
667 } else {
668 // Maybe use a filter.
669 $filter_source = [
670 'name' => null,
671 'url' => null,
672 ];
673
674 /**
675 * Provide a custom CDN source.
676 *
677 * @since 1.9.3
678 * @author Grégory Viguier
679 *
680 * @param array $filter_source {
681 * @type $name string The name of which provides the URL (plugin name, etc).
682 * @type $url string The CDN URL.
683 * }
684 */
685 $filter_source = apply_filters( 'imagify_cdn_source', $filter_source );
686
687 if ( ! empty( $filter_source['url'] ) ) {
688 $source = [
689 'source' => 'filter',
690 'name' => ! empty( $filter_source['name'] ) ? $filter_source['name'] : '',
691 'url' => $filter_source['url'],
692 ];
693 }
694 }
695
696 if ( empty( $source['url'] ) ) {
697 // No constant, no filter: use the option.
698 $source = [
699 'source' => 'option',
700 'name' => '',
701 'url' => $option_url && is_string( $option_url ) ? $option_url : get_imagify_option( 'cdn_url' ),
702 ];
703 }
704
705 if ( empty( $source['url'] ) ) {
706 // Nothing set.
707 return [
708 'source' => 'option',
709 'name' => '',
710 'url' => '',
711 ];
712 }
713
714 $source['url'] = $this->sanitize_cdn_url( $source['url'] );
715
716 if ( empty( $source['url'] ) ) {
717 // Not an URL.
718 return [
719 'source' => 'option',
720 'name' => '',
721 'url' => '',
722 ];
723 }
724
725 return $source;
726 }
727
728 /**
729 * Sanitize the CDN URL value.
730 *
731 * @since 1.9.3
732 * @access public
733 * @author Grégory Viguier
734 *
735 * @param string $url The URL to sanitize.
736 * @return string
737 */
738 public function sanitize_cdn_url( $url ) {
739 $url = sanitize_text_field( $url );
740
741 if ( ! $url || ! preg_match( '@^https?://.+\.[^.]+@i', $url ) ) {
742 // Not an URL.
743 return '';
744 }
745
746 return trailingslashit( $url );
747 }
748 }
749