PluginProbe
QuickWebP – WebP & AVIF Image Optimizer, Compression & SEO for WordPress / 3.0.0
QuickWebP – WebP & AVIF Image Optimizer, Compression & SEO for WordPress v3.0.0
4.1.1 4.1.0 4.0.1 4.0.0 3.3.1 3.3.0 trunk 1.0.0 2.0.0 2.1.0 3.0.0 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8
quickwebp / public / class-display-webp.php

class-display-webp.php in QuickWebP – WebP & AVIF Image Optimizer, Compression & SEO for WordPress 3.0.0, at public/class-display-webp.php

522 lines 14.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The handle the front-end display functionality of the plugin.
5 *
6 * @link http://webdeclic.com
7 * @since 1.0.0
8 *
9 * @package Quickwebp
10 * @subpackage Quickwebp/admin
11 */
12 class Quickwebp_Display_Webp {
13
14 /**
15 * The ID of this plugin.
16 *
17 * @since 1.0.0
18 * @access private
19 * @var string $plugin_name The ID of this plugin.
20 */
21 private $plugin_name;
22
23 /**
24 * The version of this plugin.
25 *
26 * @since 1.0.0
27 * @access private
28 * @var string $version The current version of this plugin.
29 */
30 private $version;
31
32 /**
33 * Initialize the class and set its properties.
34 *
35 * @since 1.0.0
36 * @param string $plugin_name The name of this plugin.
37 * @param string $version The version of this plugin.
38 */
39 public function __construct( $plugin_name, $version ) {
40
41 $this->plugin_name = $plugin_name;
42 $this->version = $version;
43
44 }
45
46 /**
47 * Start buffering the page content
48 */
49 public function start_content_process() {
50
51 $display_webp = get_option( 'quickwebp_settings_conversion_display_webp', quickwebp_settings_default('quickwebp_settings_conversion_display_webp') );
52 $display_webp = is_array( $display_webp ) ? $display_webp : array();
53
54 if ( !in_array( 'checked', $display_webp ) ){
55 return;
56 }
57
58 ob_start( array( $this, 'maybe_process_buffer' ) );
59
60 }
61
62 /**
63 * Maybe process the page content
64 */
65 public function maybe_process_buffer( $buffer ) {
66
67 if ( ! $this->is_html( $buffer ) ) {
68 return $buffer;
69 }
70
71 if ( strlen( $buffer ) <= 255 ) {
72 // Buffer length must be > 255 (IE does not read pages under 255 c).
73 return $buffer;
74 }
75
76 $buffer = $this->process_content( $buffer );
77
78 return $buffer;
79 }
80
81 /**
82 * Process the content
83 */
84 public function process_content( $content ) {
85
86 $html_no_picture_tags = $this->remove_picture_tags( $content );
87 $images = $this->get_images( $html_no_picture_tags );
88
89 if ( ! $images ) {
90 return $content;
91 }
92
93 foreach ( $images as $image ) {
94 $tag = $this->build_picture_tag( $image );
95 $content = str_replace( $image['tag'], $tag, $content );
96 }
97
98 return $content;
99 }
100
101 /**
102 * Remove pre-existing <picture> tags.
103 */
104 private function remove_picture_tags( $html ) {
105
106 $replace = preg_replace( '#<picture[^>]*>.*?<\/picture\s*>#mis', '', $html );
107
108 if ( null !== $replace ) {
109 return $html;
110 }
111
112 return $replace;
113 }
114
115 /**
116 * Get a list of images in a content.
117 */
118 protected function get_images( $content ) {
119
120 // Remove comments.
121 $content = preg_replace( '/<!--(.*)-->/Uis', '', $content );
122
123 if ( ! preg_match_all( '/<img\s.*>/isU', $content, $matches ) ) {
124 return [];
125 }
126
127 $images = array_map( array( $this, 'process_image' ), $matches[0] );
128 $images = array_filter( $images );
129
130 if ( ! $images || ! is_array( $images ) ) {
131 return [];
132 }
133
134 foreach ( $images as $i => $image ) {
135
136 if ( empty( $image['src']['webp_exists'] ) || empty( $image['src']['webp_url'] ) ) {
137 unset( $images[ $i ] );
138 continue;
139 }
140
141 unset( $images[ $i ]['src']['webp_path'], $images[ $i ]['src']['webp_exists'] );
142
143 if ( empty( $image['srcset'] ) || ! is_array( $image['srcset'] ) ) {
144 unset( $images[ $i ]['srcset'] );
145 continue;
146 }
147
148 foreach ( $image['srcset'] as $j => $srcset ) {
149
150 if ( ! is_array( $srcset ) ) {
151 continue;
152 }
153
154 if ( empty( $srcset['webp_exists'] ) || empty( $srcset['webp_url'] ) ) {
155 unset( $images[ $i ]['srcset'][ $j ]['webp_url'] );
156 }
157
158 unset( $images[ $i ]['srcset'][ $j ]['webp_path'], $images[ $i ]['srcset'][ $j ]['webp_exists'] );
159 }
160
161 }
162
163 return $images;
164 }
165
166 /**
167 * Tell if a content is HTML
168 */
169 protected function is_html( $content ) {
170 return preg_match( '/<\/html>/i', $content );
171 }
172
173 /**
174 * Process an image tag and get an array containing some data.
175 */
176 protected function process_image( $image ) {
177
178 $atts_pattern = '/(?<name>[^\s"\']+)\s*=\s*(["\'])\s*(?<value>.*?)\s*\2/s';
179
180 if ( ! preg_match_all( $atts_pattern, $image, $tmp_attributes, PREG_SET_ORDER ) ) {
181 // No attributes?
182 return false;
183 }
184
185 $attributes = [];
186
187 foreach ( $tmp_attributes as $attribute ) {
188 $attributes[ $attribute['name'] ] = $attribute['value'];
189 }
190
191 if ( ! empty( $attributes['class'] ) && strpos( $attributes['class'], 'quickwebp-no-webp' ) !== false ) {
192 // Has the 'quickwebp-no-webp' class.
193 return false;
194 }
195
196 // Deal with the src attribute.
197 $src_source = false;
198
199 foreach ( [ 'data-lazy-src', 'data-src', 'src' ] as $src_attr ) {
200 if ( ! empty( $attributes[ $src_attr ] ) ) {
201 $src_source = $src_attr;
202 break;
203 }
204 }
205
206 if ( ! $src_source ) {
207 // No src attribute.
208 return false;
209 }
210
211 $extensions = 'jpg|jpeg|jpe|png';
212
213 if ( ! preg_match( '@^(?<src>(?:(?:https?:)?//|/).+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $attributes[ $src_source ], $src ) ) {
214 // Not a supported image format.
215 return false;
216 }
217
218 $webp_url = $src['src'] . '.webp';
219 $webp_path = $this->url_to_path( $webp_url );
220 $webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
221
222 $data = [
223 'tag' => $image,
224 'attributes' => $attributes,
225 'src_attribute' => $src_source,
226 'src' => [
227 'url' => $attributes[ $src_source ],
228 'webp_url' => $webp_url,
229 'webp_path' => $webp_path,
230 'webp_exists' => $webp_path && @file_exists( $webp_path )
231 ],
232 'srcset_attribute' => false,
233 'srcset' => []
234 ];
235
236 // Deal with the srcset attribute.
237 $srcset_source = false;
238
239 foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) {
240 if ( ! empty( $attributes[ $srcset_attr ] ) ) {
241 $srcset_source = $srcset_attr;
242 break;
243 }
244 }
245
246 if ( $srcset_source ) {
247 $data['srcset_attribute'] = $srcset_source;
248
249 $srcset = explode( ',', $attributes[ $srcset_source ] );
250
251 foreach ( $srcset as $srcs ) {
252 $srcs = preg_split( '/\s+/', trim( $srcs ) );
253
254 if ( count( $srcs ) > 2 ) {
255 // Not a good idea to have space characters in file name.
256 $descriptor = array_pop( $srcs );
257 $srcs = [ implode( ' ', $srcs ), $descriptor ];
258 }
259
260 if ( empty( $srcs[1] ) ) {
261 $srcs[1] = '1x';
262 }
263
264 if ( ! preg_match( '@^(?<src>(?:https?:)?//.+\.(?<extension>' . $extensions . '))(?<query>\?.*)?$@i', $srcs[0], $src ) ) {
265 // Not a supported image format.
266 $data['srcset'][] = [
267 'url' => $srcs[0],
268 'descriptor' => $srcs[1],
269 ];
270 continue;
271 }
272
273 $webp_url = $src['src'] . '.webp';
274 $webp_path = $this->url_to_path( $webp_url );
275 $webp_url .= ! empty( $src['query'] ) ? $src['query'] : '';
276
277 $data['srcset'][] = [
278 'url' => $srcs[0],
279 'descriptor' => $srcs[1],
280 'webp_url' => $webp_url,
281 'webp_path' => $webp_path,
282 'webp_exists' => $webp_path && @file_exists( $webp_path )
283 ];
284 }
285 }
286
287 if ( ! $data || ! is_array( $data ) ) {
288 return false;
289 }
290
291 if ( ! isset( $data['tag'], $data['attributes'], $data['src_attribute'], $data['src'], $data['srcset_attribute'], $data['srcset'] ) ) {
292 return false;
293 }
294
295 return $data;
296 }
297
298 /**
299 * Convert a file URL to an absolute path.
300 */
301 protected function url_to_path( $url ) {
302 static $uploads_url;
303 static $uploads_dir;
304 static $root_url;
305 static $root_dir;
306 static $domain_url;
307
308 if ( ! isset( $uploads_url ) ) {
309
310 $uploads = wp_upload_dir();
311 $uploads_url = false;
312 $uploads_dir = false;
313
314 if ( false === $uploads['error'] ) {
315 $uploads_url = set_url_scheme( trailingslashit( $uploads['baseurl'] ) );
316 $uploads_dir = wp_normalize_path( trailingslashit( $uploads['basedir'] ) );
317 }
318
319 $current_network = false;
320 if ( function_exists( 'get_network' ) ) {
321 $current_network = get_network();
322 } elseif ( function_exists( 'get_current_site' ) ) {
323 $current_network = get_current_site();
324 }
325
326 if ( ! is_multisite() || is_main_site() || ! $current_network ) {
327 $root_url = home_url( '/' );
328 } else {
329
330 $root_url = is_ssl() ? 'https' : 'http';
331 $root_url = set_url_scheme( 'http://' . $current_network->domain . $current_network->path, $root_url );
332 $root_url = set_url_scheme( trailingslashit( $root_url ) );
333 }
334
335 $home = set_url_scheme( untrailingslashit( get_option( 'home' ) ), 'http' );
336 $siteurl = set_url_scheme( untrailingslashit( get_option( 'siteurl' ) ), 'http' );
337
338 if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) {
339
340 $wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */
341 $pos = strripos( str_replace( '\\', '/', ABSPATH ), trailingslashit( $wp_path_rel_to_home ) );
342 $root_path = substr( ABSPATH, 0, $pos );
343 $root_dir = trailingslashit( wp_normalize_path( $root_path ) );
344
345 } elseif ( ! defined( 'PATH_CURRENT_SITE' ) || ! is_multisite() || is_main_site()) {
346
347 $root_dir = trailingslashit( wp_normalize_path( ABSPATH ) );
348
349 } else {
350
351 $document_root = realpath( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) );
352 $document_root = trailingslashit( str_replace( '\\', '/', $document_root ) );
353 $path_current_site = trim( str_replace( '\\', '/', PATH_CURRENT_SITE ), '/' );
354 $root_dir = trailingslashit( wp_normalize_path( $document_root . $path_current_site ) );
355 }
356
357 $domain_url = wp_parse_url( $root_url );
358
359 if ( ! empty( $domain_url['scheme'] ) && ! empty( $domain_url['host'] ) ) {
360 $domain_url = $domain_url['scheme'] . '://' . $domain_url['host'] . '/';
361 } else {
362 $domain_url = false;
363 }
364
365 }
366
367 // Get the right URL format.
368 if ( $domain_url && strpos( $url, '/' ) === 0 ) {
369 // URL like `/path/to/image.jpg.webp`.
370 $url = $domain_url . ltrim( $url, '/' );
371 }
372
373 $url = set_url_scheme( $url );
374
375 // Return the path.
376 if ( stripos( $url, $uploads_url ) === 0 ) {
377 return str_ireplace( $uploads_url, $uploads_dir, $url );
378 }
379
380 if ( stripos( $url, $root_url ) === 0 ) {
381 return str_ireplace( $root_url, $root_dir, $url );
382 }
383
384 return false;
385 }
386
387 /**
388 * Build a <picture> tag to insert.
389 */
390 protected function build_picture_tag( $image ) {
391
392 $to_remove = [
393 'alt' => '',
394 'height' => '',
395 'width' => '',
396 'data-lazy-src' => '',
397 'data-src' => '',
398 'src' => '',
399 'data-lazy-srcset' => '',
400 'data-srcset' => '',
401 'srcset' => '',
402 'data-lazy-sizes' => '',
403 'data-sizes' => '',
404 'sizes' => '',
405 ];
406
407 $attributes = array_diff_key( $image['attributes'], $to_remove );
408
409 /**
410 * Remove Gutenberg specific attributes from picture tag, leave them on img tag.
411 * Optional: $attributes['class'] = 'imagify-webp-cover-wrapper'; for website admin styling ease.
412 */
413 if ( ! empty( $image['attributes']['class'] ) && strpos( $image['attributes']['class'], 'wp-block-cover__image-background' ) !== false ) {
414 unset( $attributes['style'] );
415 unset( $attributes['class'] );
416 unset( $attributes['data-object-fit'] );
417 unset( $attributes['data-object-position'] );
418 }
419
420 $output = '<picture' . $this->build_attributes( $attributes ) . ">\n";
421 $output .= $this->build_source_tag( $image );
422 $output .= $this->build_img_tag( $image );
423 $output .= "</picture>\n";
424
425 return $output;
426 }
427
428 /**
429 * Create HTML attributes from an array.
430 */
431 protected function build_attributes( $attributes ) {
432
433 if ( ! $attributes || ! is_array( $attributes ) ) {
434 return '';
435 }
436
437 $out = '';
438
439 foreach ( $attributes as $attribute => $value ) {
440 $out .= ' ' . $attribute . '="' . esc_attr( $value ) . '"';
441 }
442
443 return $out;
444 }
445
446 /**
447 * Build the <source> tag to insert in the <picture>.
448 */
449 protected function build_source_tag( $image ) {
450
451 $srcset_source = ! empty( $image['srcset_attribute'] ) ? $image['srcset_attribute'] : $image['src_attribute'] . 'set';
452 $attributes = [
453 'type' => 'image/webp',
454 $srcset_source => [],
455 ];
456
457 if ( ! empty( $image['srcset'] ) ) {
458 foreach ( $image['srcset'] as $srcset ) {
459 if ( empty( $srcset['webp_url'] ) ) {
460 continue;
461 }
462
463 $attributes[ $srcset_source ][] = $srcset['webp_url'] . ' ' . $srcset['descriptor'];
464 }
465 }
466
467 if ( empty( $attributes[ $srcset_source ] ) ) {
468 $attributes[ $srcset_source ][] = $image['src']['webp_url'];
469 }
470
471 $attributes[ $srcset_source ] = implode( ', ', $attributes[ $srcset_source ] );
472
473 foreach ( [ 'data-lazy-srcset', 'data-srcset', 'srcset' ] as $srcset_attr ) {
474 if ( ! empty( $image['attributes'][ $srcset_attr ] ) && $srcset_attr !== $srcset_source ) {
475 $attributes[ $srcset_attr ] = $image['attributes'][ $srcset_attr ];
476 }
477 }
478
479 if ( 'srcset' !== $srcset_source && empty( $attributes['srcset'] ) && ! empty( $image['attributes']['src'] ) ) {
480 // Lazyload: the "src" attr should contain a placeholder (a data image or a blank.gif ).
481 $attributes['srcset'] = $image['attributes']['src'];
482 }
483
484 foreach ( [ 'data-lazy-sizes', 'data-sizes', 'sizes' ] as $sizes_attr ) {
485 if ( ! empty( $image['attributes'][ $sizes_attr ] ) ) {
486 $attributes[ $sizes_attr ] = $image['attributes'][ $sizes_attr ];
487 }
488 }
489
490 return '<source' . $this->build_attributes( $attributes ) . "/>\n";
491 }
492
493 /**
494 * Build the <img> tag to insert in the <picture>.
495 */
496 protected function build_img_tag( $image ) {
497 /**
498 * Gutenberg fix.
499 * Check for the 'wp-block-cover__image-background' class on the original image, and leave that class and style attributes if found.
500 */
501 if ( ! empty( $image['attributes']['class'] ) && strpos( $image['attributes']['class'], 'wp-block-cover__image-background' ) !== false ) {
502 $to_remove = [
503 'id' => '',
504 'title' => '',
505 ];
506
507 $attributes = array_diff_key( $image['attributes'], $to_remove );
508 } else {
509 $to_remove = [
510 'class' => '',
511 'id' => '',
512 'style' => '',
513 'title' => '',
514 ];
515
516 $attributes = array_diff_key( $image['attributes'], $to_remove );
517 }
518
519 return '<img' . $this->build_attributes( $attributes ) . "/>\n";
520 }
521
522 }