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

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