PluginProbe ʕ •ᴥ•ʔ
WP Popular Posts / 7.4.0
WP Popular Posts v7.4.0
4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 5.0.0 5.0.1 5.0.2 5.1.0 5.2.0 5.2.1 5.2.2 5.2.3 5.2.4 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 6.0.0 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.1.0 6.1.1 6.1.2 6.1.3 6.1.4 6.2.0 6.2.1 6.3.0 6.3.1 6.3.2 6.3.3 6.3.4 6.4.0 6.4.1 6.4.2 7.0.0 7.0.1 7.1.0 7.2.0 7.3.0 7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 7.3.6 7.3.7 7.3.8 7.4.0 trunk 2.3.7 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.2 4.0.3 4.0.5 4.0.6
wordpress-popular-posts / src / Image.php
wordpress-popular-posts / src Last commit date
Activation 8 months ago Admin 3 weeks ago Block 3 weeks ago Compatibility 8 months ago Container 8 months ago Front 1 year ago Rest 8 months ago Shortcode 1 year ago Traits 4 years ago Widget 3 weeks ago Bootstrap.php 5 years ago Cache.php 8 months ago Helper.php 3 weeks ago I18N.php 5 years ago Image.php 8 months ago Output.php 3 weeks ago Query.php 3 weeks ago Settings.php 3 weeks ago Themer.php 8 months ago Translate.php 3 years ago Upgrader.php 8 months ago WordPressPopularPosts.php 8 months ago deprecated.php 4 years ago template-tags.php 3 weeks ago
Image.php
979 lines
1 <?php
2 /**
3 * This class builds/retrieves the thumbnail image of each popular posts.
4 *
5 *
6 * @package WordPressPopularPosts
7 * @author Hector Cabrera <me@cabrerahector.com>
8 */
9
10 namespace WordPressPopularPosts;
11
12 class Image {
13
14 /**
15 * Default thumbnail.
16 *
17 * @since 2.2.0
18 * @var string
19 */
20 private $default_thumbnail = '';
21
22 /**
23 * Plugin uploads directory.
24 *
25 * @since 3.0.4
26 * @var array
27 */
28 private $uploads_dir = [];
29
30 /**
31 * Admin settings.
32 *
33 * @since 5.0.0
34 * @var array
35 */
36 private $admin_options = [];
37
38 /**
39 * Available image sizes.
40 *
41 * @since 5.0.0
42 * @var array
43 */
44 private $available_sizes = [];
45
46 /**
47 * Available image descriptors.
48 *
49 * @since 5.3.0
50 * @var array
51 */
52 private $descriptors = [];
53
54 /**
55 * Construct.
56 *
57 * @since 4.0.0
58 * @param array $admin_options
59 */
60 public function __construct(array $admin_options)
61 {
62 $this->admin_options = $admin_options;
63
64 // Set default thumbnail
65 $this->default_thumbnail = plugins_url('assets/images/no_thumb.jpg', dirname(__FILE__, 1));
66
67 if ( Helper::is_image_url($this->admin_options['tools']['thumbnail']['default']) ) {
68 $this->default_thumbnail = $this->admin_options['tools']['thumbnail']['default'];
69 }
70
71 // Set uploads folder
72 $wp_upload_dir = wp_get_upload_dir();
73 $this->uploads_dir['basedir'] = $wp_upload_dir['basedir'] . '/wordpress-popular-posts';
74 $this->uploads_dir['baseurl'] = $wp_upload_dir['baseurl'] . '/wordpress-popular-posts';
75
76 if ( ! is_dir($this->uploads_dir['basedir']) ) {
77 // Couldn't create the folder, store thumbnails in Uploads
78 if ( ! wp_mkdir_p($this->uploads_dir['basedir']) ) {
79 $this->uploads_dir['basedir'] = $wp_upload_dir['basedir'];
80 $this->uploads_dir['baseurl'] = $wp_upload_dir['baseurl'];
81 }
82 }
83
84 // Set descriptors
85 $this->descriptors = ['1.5', '2', '2.5', '3'];
86 }
87
88 /**
89 * Get WPP's uploads folder.
90 *
91 * @since 4.0.0
92 * @access public
93 * @return array|bool
94 */
95 public function get_plugin_uploads_dir()
96 {
97 if ( is_array($this->uploads_dir) && ! empty($this->uploads_dir) ) {
98 return $this->uploads_dir;
99 }
100 return false;
101 }
102
103 /**
104 * Returns an image.
105 *
106 * @since 5.0.0
107 * @param int $post_id Post ID
108 * @param array $size Image size (width & height)
109 * @param string $source Image source
110 * @param bool|array $crop Whether to crop the image or not, and if so how
111 * @param string $build Whether to build the image or get an existing one
112 * @return string
113 */
114 public function get(int $post_id, array $size, string $source, $crop = true, ?string $build = 'manual') /** @TODO: starting PHP 8.0 $crop can be declared as mixed $crop */
115 {
116 // Bail, $post_id is not an integer
117 if ( ! is_numeric($post_id) ) {
118 return '';
119 }
120
121 // Let's make sure that the image source is a known one
122 if (
123 ! in_array(
124 $source,
125 ['featured', 'first_image', 'first_attachment', 'custom_field']
126 )
127 ) {
128 $source = 'featured';
129 }
130
131 // Let's make sure that we're getting integer values here
132 $size = array_map('absint', $size);
133 $size = array_map(function($dimension) {
134 return $dimension <= 0 ? 75 : $dimension;
135 }, $size);
136
137 $alt = '';
138 $classes = ['wpp-thumbnail', 'wpp_' . $source];
139 $filename = $post_id . '-' . $source . '-' . $size[0] . 'x' . $size[1];
140 $cached = $this->exists($filename);
141
142 // We have a thumbnail already, return it
143 if ( $cached ) {
144 $classes[] = 'wpp_cached_thumb';
145
146 /**
147 * Filters CSS classes assigned to the thumbnail
148 *
149 * @since 5.0.0
150 * @param array CSS classes
151 * @param int The post ID
152 * @return array The new CSS classes
153 */
154 $classes = apply_filters(
155 'wpp_thumbnail_class_attribute',
156 $classes,
157 $post_id
158 );
159
160 /**
161 * Filters ALT attribute assigned to the thumbnail
162 *
163 * @since 5.0.0
164 * @param string Original ALT attribute
165 * @param int The post ID
166 * @return string The new ALT attribute
167 */
168 $alt = apply_filters(
169 'wpp_thumbnail_alt_attribute',
170 $this->get_alt_attribute($post_id, $source),
171 $post_id
172 );
173
174 return $this->render(
175 $post_id,
176 $cached,
177 $size,
178 is_array($classes) ? implode(' ', $classes) : 'wpp-thumbnail wpp_' . $source,
179 is_string($alt) ? $alt : ''
180 );
181 }
182
183 $thumb_url = null;
184
185 // Return image as-is, no need to create a new thumbnail
186 if (
187 ( 'custom_field' == $source && ! $this->admin_options['tools']['thumbnail']['resize'] )
188 || ( 'featured' == $source && 'predefined' == $build )
189 ){
190 // Get custom field image URL
191 if ( 'custom_field' == $source && ! $this->admin_options['tools']['thumbnail']['resize'] ) {
192 $thumb_url = get_post_meta(
193 $post_id,
194 $this->admin_options['tools']['thumbnail']['field'],
195 true
196 );
197
198 if ( ! $thumb_url || ! Helper::is_image_url($thumb_url) ) {
199 // Is this an attachment ID instead of an image URL?
200 if ( Helper::is_number($thumb_url) ) {
201 $thumb_url = wp_get_attachment_image_src($thumb_url, 'full');
202 $thumb_url = is_array($thumb_url) ? $thumb_url[0] : null;
203 } else {
204 $thumb_url = null;
205 }
206 }
207 }
208 // Get Post Thumbnail
209 else {
210 if (
211 current_theme_supports('post-thumbnails')
212 && has_post_thumbnail($post_id)
213 ) {
214 // Find corresponding image size
215 $stock_size = null;
216 $images_sizes = $this->get_sizes(null);
217
218 foreach ( $images_sizes as $name => $attr ) :
219 if (
220 $attr['width'] == $size[0]
221 && $attr['height'] == $size[1]
222 && $attr['crop'] == $crop
223 ) {
224 $stock_size = $name;
225 break;
226 }
227 endforeach;
228
229 // Couldn't find a matching size so let's go with width/height combo instead
230 // (this should never happen but better safe than sorry!)
231 if ( null == $stock_size ) {
232 $stock_size = $size;
233 }
234
235 /**
236 * Filters CSS classes assigned to the thumbnail
237 *
238 * @since 5.0.0
239 * @param array CSS classes
240 * @param int The post ID
241 * @return array The new CSS classes
242 */
243 $classes = apply_filters(
244 'wpp_thumbnail_class_attribute',
245 $classes,
246 $post_id
247 );
248
249 $featured_image = get_the_post_thumbnail(
250 $post_id,
251 $stock_size
252 );
253
254 if ( strpos($featured_image, 'class="') && is_array($classes) && ! empty($classes) ) {
255 $featured_image = str_replace('class="', 'class="' . esc_attr(implode(' ', $classes)) . ' ', $featured_image);
256 }
257
258 if ( $this->admin_options['tools']['thumbnail']['lazyload'] && false == strpos($featured_image, 'loading="lazy"') ) {
259 $featured_image = str_replace('src="', 'loading="lazy" src="', $featured_image);
260 }
261
262 return $featured_image;
263 }
264 }
265 }
266 // Build a new thumbnail and return it
267 else {
268 $file_path = null;
269
270 if ( 'custom_field' == $source && $this->admin_options['tools']['thumbnail']['resize'] ) {
271 $thumb_url = get_post_meta(
272 $post_id,
273 $this->admin_options['tools']['thumbnail']['field'],
274 true
275 );
276
277 if ( ! $thumb_url || ! Helper::is_image_url($thumb_url) ) {
278 // Is this an attachment ID instead of an image URL?
279 // If so, try to fetch the image
280 if ( Helper::is_number($thumb_url) ) {
281 $thumb_url = wp_get_attachment_image_src($thumb_url, 'full');
282 $thumb_url = is_array($thumb_url) ? $thumb_url[0] : null;
283 } else {
284 $thumb_url = null;
285 }
286 }
287
288 if ( $thumb_url && Helper::is_image_url($thumb_url) ) {
289 $file_path = $this->url_to_path($thumb_url, $post_id);
290 }
291 } else {
292 $file_meta = $this->get_file_meta($post_id, $source);
293
294 if ( is_array($file_meta) && isset($file_meta['path']) ) {
295 $alt = isset($file_meta['alt']) ? $file_meta['alt'] : '';
296 $file_path = $file_meta['path'];
297 }
298 }
299
300 if ( $file_path ) {
301 $extension = pathinfo($file_path, PATHINFO_EXTENSION);
302 $thumb_url = $this->resize(
303 $file_path,
304 $filename . '.' . $extension,
305 $size,
306 $crop
307 );
308 }
309 }
310
311 if ( ! $thumb_url ) {
312 $classes[] = 'wpp_def_no_src';
313 $thumb_url = $this->get_default_url($post_id);
314 }
315
316 /**
317 * Filters CSS classes assigned to the thumbnail
318 *
319 * @since 5.0.0
320 * @param array CSS classes
321 * @param int The post ID
322 * @return array The new CSS classes
323 */
324 $classes = apply_filters(
325 'wpp_thumbnail_class_attribute',
326 $classes,
327 $post_id
328 );
329
330 /**
331 * Filters ALT attribute assigned to the thumbnail
332 *
333 * @since 5.0.0
334 * @param string Original ALT attribute
335 * @param int The post ID
336 * @return string The new ALT attribute
337 */
338 $alt = apply_filters(
339 'wpp_thumbnail_alt_attribute',
340 $this->get_alt_attribute($post_id, $source),
341 $post_id
342 );
343
344 return $this->render(
345 $post_id,
346 $thumb_url,
347 $size,
348 is_array($classes) ? implode(' ', $classes) : 'wpp-thumbnail wpp_' . $source,
349 is_string($alt) ? $alt : ''
350 );
351 }
352
353 /**
354 * Checks whether a given thumbnail exists.
355 *
356 * @since 5.0.0
357 * @access private
358 * @param string $filename
359 * @return string|bool Full URL to image
360 */
361 private function exists(string $filename)
362 {
363 // Do we have thumbnail already?
364 $file = $this->resolve(trailingslashit($this->get_plugin_uploads_dir()['basedir']) . $filename);
365
366 if ( $file && is_file($file) ) {
367 $extension = pathinfo($file, PATHINFO_EXTENSION);
368 return trailingslashit($this->get_plugin_uploads_dir()['baseurl']) . $filename . '.' . $extension;
369 }
370
371 return false;
372 }
373
374 /**
375 * Resolves filename.
376 *
377 * @since 5.0.0
378 * @access private
379 * @author Ioan Chiriac
380 * @link https://stackoverflow.com/a/29468093/9131961
381 * @param string $name
382 * @return string|bool Resolved path, or false if not found
383 */
384 private function resolve(string $name)
385 {
386 $info = pathinfo($name);
387
388 // File already contains an extension, return it
389 if ( isset($info['extension']) && ! empty($info['extension']) ) {
390 return $name;
391 }
392
393 $filename = $info['filename'];
394 $len = strlen($filename);
395
396 // open the folder
397 $dh = opendir($info['dirname']);
398
399 if ( ! $dh ) {
400 return false;
401 }
402
403 // scan each file in the folder
404 while ( ($file = readdir($dh)) !== false ) {
405 if ( strncmp($file, $filename, $len) === 0 ) {
406 if ( strlen($name) > $len ) {
407 // if name contains a directory part
408 $name = substr($name, 0, strlen($name) - $len) . $file;
409 } else {
410 // if the name is at the path root
411 $name = $file;
412 }
413
414 closedir($dh);
415 return $name;
416 }
417 }
418
419 // file not found
420 closedir($dh);
421 return false;
422 }
423
424 /**
425 * Retrieves local path to image.
426 *
427 * @since 5.0.0
428 * @access private
429 * @param string $url
430 * @param integer $post_ID
431 * @return string|boolean Path to image, or false if not found
432 */
433 private function url_to_path(string $url, ?int $post_ID)
434 {
435 if ( Helper::is_image_url($url) ) {
436 $attachment_id = $this->get_attachment_id($url);
437
438 // Image is hosted locally
439 if ( $attachment_id ) {
440 return get_attached_file($attachment_id);
441 }
442
443 // Image hosted elsewhere?
444 if ( $post_ID && Helper::is_number($post_ID) ) {
445 return $this->fetch_external_image($post_ID, $url);
446 }
447 }
448
449 return false;
450 }
451
452 /**
453 * Gets image meta.
454 *
455 * @since 5.0.0
456 * @access private
457 * @param int $id Post ID
458 * @param string $source Image source
459 * @return array|bool
460 */
461 private function get_file_meta(int $id, string $source)
462 {
463 // get thumbnail path from the Featured Image
464 if ( 'featured' == $source ) {
465 $thumbnail_id = get_post_thumbnail_id($id);
466
467 if ( $thumbnail_id ) {
468 // image path
469 return [
470 'path' => get_attached_file($thumbnail_id),
471 'alt' => get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true)
472 ];
473 }
474 }
475 // get thumbnail path from first image attachment
476 elseif ( 'first_attachment' == $source ) {
477 $args = [
478 'numberposts' => 1,
479 'order' => 'ASC',
480 'post_parent' => $id,
481 'post_type' => 'attachment',
482 'post_mime_type' => 'image'
483 ];
484 $post_attachments = get_children($args);
485
486 if ( ! empty($post_attachments) ) {
487 $first_img = array_shift($post_attachments);
488
489 return [
490 'path' => get_attached_file($first_img->ID),
491 'alt' => get_post_meta($first_img->ID, '_wp_attachment_image_alt', true)
492 ];
493 }
494 }
495 // get thumbnail path from post content
496 elseif ( 'first_image' == $source ) {
497 $content = Helper::get_post_content($id);
498
499 if ( $content ) {
500 // at least one image has been found
501 if ( preg_match('/<img[^>]+>/i', $content, $img) ) {
502 // get img src attribute from the first image found
503 preg_match('/(src)="([^"]*)"/i', $img[0], $src_attr);
504
505 if ( isset($src_attr[2]) && ! empty($src_attr[2]) ) {
506 // get img alt attribute from the first image found
507 $alt = '';
508 preg_match('/(alt)="([^"]*)"/i', $img[0], $alt_attr);
509
510 if ( isset($alt_attr[2]) && ! empty($alt_attr[2]) ) {
511 $alt = $alt_attr[2];
512 }
513
514 // image from Media Library
515 $attachment_id = $this->get_attachment_id($src_attr[2]);
516
517 if ( $attachment_id ) {
518 return [
519 'path' => get_attached_file($attachment_id),
520 'alt' => $alt
521 ];
522 } // external image?
523 else {
524 return [
525 'path' => $this->fetch_external_image($id, $src_attr[2]),
526 'alt' => $alt
527 ];
528 }
529 }
530 }
531 }
532 }
533
534 return false;
535 }
536
537 /**
538 * Gets image ALT attribute.
539 *
540 * @since 5.0.0
541 * @access private
542 * @param int $id Post ID
543 * @param string $source Image source
544 * @return string
545 */
546 private function get_alt_attribute(int $id, string $source)
547 {
548 $alt = '';
549
550 // get thumbnail path from the Featured Image
551 if ( 'featured' == $source ) {
552 $thumbnail_id = get_post_thumbnail_id($id);
553
554 if ( $thumbnail_id ) {
555 // image path
556 $alt = get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true);
557 }
558 }
559 // get thumbnail path from first image attachment
560 elseif ( 'first_attachment' == $source ) {
561 $args = [
562 'numberposts' => 1,
563 'order' => 'ASC',
564 'post_parent' => $id,
565 'post_type' => 'attachment',
566 'post_mime_type' => 'image'
567 ];
568 $post_attachments = get_children($args);
569
570 if ( ! empty($post_attachments) ) {
571 $first_img = array_shift($post_attachments);
572 $alt = get_post_meta($first_img->ID, '_wp_attachment_image_alt', true);
573 }
574 }
575 // get thumbnail path from post content
576 elseif ( 'first_image' == $source ) {
577 $content = Helper::get_post_content($id);
578
579 if ( $content ) {
580 // at least one image has been found
581 if ( preg_match('/<img[^>]+>/i', $content, $img) ) {
582 // get img alt attribute from the first image found
583 preg_match('/(alt)="([^"]*)"/i', $img[0], $alt_attr);
584
585 if ( isset($alt_attr[2]) && ! empty($alt_attr[2]) ) {
586 $alt = $alt_attr[2];
587 }
588 }
589 }
590 }
591
592 return $alt;
593 }
594
595 /**
596 * Get the Attachment ID for a given image URL.
597 *
598 * @since 3.0.0
599 * @access private
600 * @author Frankie Jarrett
601 * @link http://frankiejarrett.com/get-an-attachment-id-by-url-in-wordpress/
602 * @param string $url
603 * @return int|null
604 */
605 private function get_attachment_id(string $url)
606 {
607 $url = Helper::add_scheme(
608 $url,
609 is_ssl() ? 'https://' : 'http://'
610 );
611
612 // Split the $url into two parts with the wp-content directory as the separator.
613 $parse_url = explode(parse_url(WP_CONTENT_URL, PHP_URL_PATH), $url);
614
615 // Get the host of the current site and the host of the $url, ignoring www.
616 $this_host = str_ireplace('www.', '', parse_url(home_url(), PHP_URL_HOST));
617 $file_host = str_ireplace('www.', '', parse_url($url, PHP_URL_HOST));
618
619 // Return nothing if there aren't any $url parts or if the current host and $url host do not match.
620 if (
621 ! isset($parse_url[1])
622 || empty($parse_url[1])
623 || ($this_host != $file_host)
624 ) {
625 return false;
626 }
627
628 // Now we're going to quickly search the DB for any attachment GUID with a partial path match.
629 // Example: /uploads/2013/05/test-image.jpg
630 global $wpdb;
631 $posts_table = "{$wpdb->prefix}posts";
632
633 $attachment = $wpdb->get_col($wpdb->prepare("SELECT ID FROM %i WHERE guid RLIKE %s;", $posts_table, $parse_url[1])); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
634
635 if ( ! $attachment ) {
636 // Maybe it's a resized image, so try to get the full one
637 $parse_url[1] = preg_replace('/-[0-9]{1,4}x[0-9]{1,4}\.(jpg|jpeg|png|gif|bmp)$/i', '.$1', $parse_url[1]);
638 $attachment = $wpdb->get_col($wpdb->prepare("SELECT ID FROM %i WHERE guid RLIKE %s;", $posts_table, $parse_url[1])); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
639 }
640
641 // Returns null if no attachment is found.
642 return isset($attachment[0]) ? $attachment[0] : null;
643 }
644
645 /**
646 * Fetchs external images.
647 *
648 * @since 2.3.3
649 * @access private
650 * @param int $id Post ID.
651 * @param string $url Image url.
652 * @return string|bool Image path, or false on failure.
653 */
654 private function fetch_external_image(int $id, string $url)
655 {
656 if ( ! Helper::is_image_url($url) ) {
657 return false;
658 }
659
660 $full_image_path = trailingslashit($this->get_plugin_uploads_dir()['basedir']) . "{$id}_" . sanitize_file_name(rawurldecode(wp_basename($url)));
661
662 // if the file exists already, return URL and path
663 if ( file_exists($full_image_path) ) {
664 return $full_image_path;
665 }
666
667 $url = Helper::add_scheme(
668 $url,
669 is_ssl() ? 'https://' : 'http://'
670 );
671
672 $accepted_status_codes = [200, 301, 302];
673 $response = wp_remote_head($url, ['timeout' => 5, 'sslverify' => false]);
674
675 if (
676 ! is_wp_error($response)
677 && in_array(wp_remote_retrieve_response_code($response), $accepted_status_codes)
678 ) {
679 require_once ABSPATH . 'wp-admin/includes/file.php';
680
681 $url = str_replace('https://', 'http://', $url);
682 $tmp = download_url($url);
683
684 // File was downloaded successfully
685 if ( ! is_wp_error($tmp) ) {
686 // Determine image type
687 if ( function_exists('exif_imagetype') ) {
688 $image_type = exif_imagetype($tmp);
689 } else {
690 $image_type = getimagesize($tmp);
691 $image_type = ( isset($image_type[2]) ) ? $image_type[2] : null;
692 }
693
694 // Valid image, save it
695 if ( in_array($image_type, [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP, IMAGETYPE_AVIF]) ) {
696 // move file to Uploads
697 if ( @rename($tmp, $full_image_path) ) {
698 // borrowed from WP - set correct file permissions
699 $stat = stat(dirname($full_image_path));
700 $perms = $stat['mode'] & 0000644;
701 @chmod($full_image_path, $perms);
702
703 return $full_image_path;
704 }
705 }
706
707 // Invalid file, remove it
708 @unlink($tmp);
709 }
710 }
711
712 return false;
713 }
714
715 /**
716 * Creates thumbnails.
717 *
718 * @since 3.0.0
719 * @access private
720 * @param string $path Image path
721 * @param string $filename Image filename
722 * @param array $size Image size
723 * @param bool $crop Whether to crop the image or not
724 * @return string|bool Image URL on success, false on error
725 */
726 private function resize(string $path, string $filename, array $size, bool $crop = true)
727 {
728 $image = wp_get_image_editor($path);
729
730 // valid image, create thumbnails
731 if ( ! is_wp_error($image) ) {
732 $original_size = $image->get_size();
733 $sizes = [
734 '1x' => $size
735 ];
736 $thumbnail = '';
737
738 /**
739 * Hook to enable/disable retina support.
740 * @since 5.3.0
741 */
742 $retina_support = apply_filters('wpp_retina_support', true);
743
744 if ( $retina_support ) {
745 // Calculate thumbnail sizes
746 foreach( $this->descriptors as $descriptor ) {
747 $new_size_width = floor($descriptor * $size[0]);
748 $new_size_height = floor($descriptor * $size[1]);
749
750 if (
751 $new_size_width <= $original_size['width']
752 && $new_size_height <= $original_size['height']
753 ) {
754 $sizes[$descriptor . 'x'] = [$new_size_width, $new_size_height];
755 }
756 }
757 }
758
759 $path_parts = null;
760
761 // Generate thumbnails
762 foreach( $sizes as $d => $s ) {
763 if ( '1x' == $d ) {
764 $thumbnail = $this->generate_thumbnail($path, $filename, $s, $crop);
765
766 // Image could not be generated, let's bail early.
767 if ( ! $thumbnail ) {
768 break;
769 }
770 } else {
771 if ( ! $path_parts ) {
772 $path_parts = pathinfo($filename);
773 }
774
775 $filename_with_descriptor = $path_parts['filename'] . "@{$d}." . $path_parts['extension'];
776 $this->generate_thumbnail($path, $filename_with_descriptor, $s, $crop);
777 }
778 }
779
780 return $thumbnail;
781 }
782
783 return false;
784 }
785
786 /**
787 * Creates image.
788 *
789 * @since 5.3.0
790 * @access private
791 * @param string $path Image path
792 * @param string $filename Image filename
793 * @param array $size Image size
794 * @param bool $crop Whether to crop the image or not
795 * @return string|bool Image URL on success, false on error
796 */
797 private function generate_thumbnail(string $path, string $filename, array $size, bool $crop = true)
798 {
799 $image = wp_get_image_editor($path);
800
801 // valid image, create thumbnail
802 if ( ! is_wp_error($image) ) {
803 /**
804 * Hook to change the image compression quality of WPP's thumbnails.
805 * @since 4.2.1
806 */
807 $quality = apply_filters('wpp_thumbnail_compression_quality', null);
808
809 if ( $quality && ! ctype_digit( (string) $quality) ) {
810 $quality = null; // Fallback to core's default
811 }
812
813 $image->set_quality($quality);
814 $image->resize($size[0], $size[1], $crop);
815
816 $mime_type = null;
817
818 // .webp thumbnails?
819 if (
820 'webp' === $this->admin_options['tools']['thumbnail']['format']
821 && $image->supports_mime_type('image/webp') // .webp support requires WP 5.8 or higher
822 ) {
823 $filename = substr($filename, 0, strrpos($filename, '.')) . '.webp';
824 $mime_type = 'image/webp';
825 }
826 // .avif thumbnails?
827 elseif (
828 'avif' === $this->admin_options['tools']['thumbnail']['format']
829 && $image->supports_mime_type('image/avif') // .avif support requires WP 6.5 or higher
830 ) {
831 $filename = substr($filename, 0, strrpos($filename, '.')) . '.avif';
832 $mime_type = 'image/avif';
833 }
834
835 $file_path = trailingslashit($this->get_plugin_uploads_dir()['basedir']) . $filename;
836 $new_img = $image->save($file_path, $mime_type);
837
838 if ( ! is_wp_error($new_img) ) {
839 return trailingslashit($this->get_plugin_uploads_dir()['baseurl']) . $filename;
840 }
841 }
842
843 return false;
844 }
845
846 /**
847 * Generates srcset attribute for this image.
848 *
849 * @since 5.3.0
850 * @param string $src
851 * @return string
852 */
853 private function get_srcset(string $src)
854 {
855 /**
856 * Hook to enable/disable retina support.
857 * @since 5.3.0
858 */
859 $retina_support = apply_filters('wpp_retina_support', true);
860
861 if ( ! $retina_support ) {
862 return '';
863 }
864
865 $path_parts = pathinfo($src);
866 $srcset = [$src];
867
868 foreach( $this->descriptors as $descriptor ) {
869 $d = "{$descriptor}x";
870 $filename = $path_parts['filename'] . "@{$d}." . $path_parts['extension'];
871
872 if ( @file_exists(trailingslashit($this->get_plugin_uploads_dir()['basedir']) . $filename) ) {
873 $srcset[] = $path_parts['dirname'] . '/' . $filename . ' ' . $d;
874 }
875 }
876
877 return ( count($srcset) > 1 ) ? ' srcset="' . implode(', ', $srcset) . '" ' : '';
878 }
879
880 /**
881 * Render image tag.
882 *
883 * @since 3.0.0
884 * @access public
885 * @param int $post_id The post/page ID
886 * @param string $src Image URL
887 * @param array $dimension Image's width and height
888 * @param string $class CSS class
889 * @param object $alt Alternative text
890 * @param string $error Error, if the image could not be created
891 * @return string
892 */
893 public function render(int $post_id, string $src, array $size, string $class, string $alt = '', string $error = '')
894 {
895 $img_tag = '';
896
897 if ( $error ) {
898 $img_tag = '<!-- ' . esc_html($error) . ' --> ';
899 }
900
901 // Make sure we use the right protocol
902 $src = esc_url(is_ssl() ? str_ireplace('http://', 'https://', $src) : $src);
903 // Get srcset, if available
904 $srcset = $this->get_srcset($src);
905
906 $src = 'src="' . $src . '"' . $srcset;
907
908 // Lazy Load attribute, if enabled
909 $lazyload = ( $this->admin_options['tools']['thumbnail']['lazyload'] ) ? ' loading="lazy"' : '';
910
911 $img_tag .= '<img ' . $src . ' width="' . esc_attr($size[0]) . '" height="' . esc_attr($size[1]) . '" alt="' . esc_attr($alt) . '" class="' . esc_attr($class) . '" decoding="async" ' . $lazyload . ' />';
912
913 return apply_filters('wpp_render_image', $img_tag, $post_id);
914 }
915
916 /**
917 * Gets list of available thumbnails sizes
918 *
919 * @since 3.2.0
920 * @link http://codex.wordpress.org/Function_Reference/get_intermediate_image_sizes
921 * @param string $size
922 * @return array|bool
923 */
924 public function get_sizes(?string $size)
925 {
926 if ( ! is_array($this->available_sizes) || empty($this->available_sizes) ) {
927 global $_wp_additional_image_sizes;
928
929 $this->available_sizes = [];
930 $get_intermediate_image_sizes = get_intermediate_image_sizes();
931
932 // Create the full array with sizes and crop info
933 foreach( $get_intermediate_image_sizes as $_size ) {
934 if ( in_array($_size, ['thumbnail', 'medium', 'large']) ) {
935 $this->available_sizes[$_size]['width'] = get_option($_size . '_size_w');
936 $this->available_sizes[$_size]['height'] = get_option($_size . '_size_h');
937 $this->available_sizes[$_size]['crop'] = (bool) get_option($_size . '_crop');
938 } elseif ( isset($_wp_additional_image_sizes[$_size]) ) {
939 $this->available_sizes[$_size] = [
940 'width' => $_wp_additional_image_sizes[$_size]['width'],
941 'height' => $_wp_additional_image_sizes[$_size]['height'],
942 'crop' => $_wp_additional_image_sizes[$_size]['crop']
943 ];
944 }
945 }
946 }
947
948 // Get only 1 size if found
949 if ( $size ) {
950 if ( isset($this->available_sizes[$size]) ) {
951 return $this->available_sizes[$size];
952 }
953 return false;
954 }
955
956 return $this->available_sizes;
957 }
958
959 /**
960 * Returns the URL of the default thumbnail image.
961 *
962 * @since 5.0.0
963 * @param int|null
964 * @return string
965 */
966 public function get_default_url(?int $post_ID = null)
967 {
968 if ( has_filter('wpp_default_thumbnail_url') ) {
969 $default_thumbnail_url = apply_filters('wpp_default_thumbnail_url', $this->default_thumbnail, $post_ID);
970
971 if ( $default_thumbnail_url != $this->default_thumbnail && Helper::is_image_url($default_thumbnail_url) ) {
972 return $default_thumbnail_url;
973 }
974 }
975
976 return $this->default_thumbnail;
977 }
978 }
979