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

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