PluginProbe
QuickWebP – WebP & AVIF Image Optimizer, Compression & SEO for WordPress / 3.3.0
QuickWebP – WebP & AVIF Image Optimizer, Compression & SEO for WordPress v3.3.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.3.0, at public/class-display-webp.php

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