PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / trunk
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization vtrunk
4.2.14 4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 3.12.0 All 135 releases
← All changes | inc/tag_replacer.php +535 -74 3.11.0 → trunk View file →
@@ -1,6 +1,8 @@
1 1 <?php
2 2
3 +use OptimoleWP\Preload\Links;
4 +
3 5 /**
4 6 * The class handles the img tag replacements.
5 7 *
6 8 * @package \Optml\Inc
@@ -8,8 +10,9 @@
8 10 */
9 11 final class Optml_Tag_Replacer extends Optml_App_Replacer {
10 12 use Optml_Normalizer;
11 13 use Optml_Validator;
14 + use Optml_Dam_Offload_Utils;
12 15
13 16 /**
14 17 * Cached object instance.
15 18 *
@@ -45,31 +48,89 @@
45 48 /**
46 49 * The initialize method.
47 50 */
48 51 public function init() {
49 -
50 52 parent::init();
51 53 add_filter( 'optml_content_images_tags', [ $this, 'process_image_tags' ], 1, 2 );
52 54
53 - if ( $this->settings->use_lazyload() ) {
54 - return;
55 + if ( ! $this->settings->use_lazyload() ) {
56 + add_filter( 'optml_tag_replace', [ $this, 'regular_tag_replace' ], 1, 6 );
55 57 }
56 -
57 - add_filter( 'optml_tag_replace', [ $this, 'regular_tag_replace' ], 1, 6 );
58 58 add_filter( 'image_downsize', [ $this, 'filter_image_downsize' ], PHP_INT_MAX, 3 );
59 - add_filter( 'wp_calculate_image_srcset', [ $this, 'filter_srcset_attr' ], PHP_INT_MAX, 5 );
59 + add_filter( 'wp_calculate_image_srcset', [ $this, 'filter_srcset_attr' ], PHP_INT_MAX - 1, 5 );
60 60 add_filter( 'wp_calculate_image_sizes', [ $this, 'filter_sizes_attr' ], 1, 2 );
61 + add_filter( 'wp_image_src_get_dimensions', [ $this, 'filter_image_src_get_dimensions' ], 99, 4 );
62 + if ( $this->settings->get( 'retina_images' ) === 'enabled' ) {
63 + add_filter( 'wp_get_attachment_image_attributes', [ $this, 'filter_attachment_image_attributes' ], 99, 3 );
64 + }
65 + }
61 66
67 + /**
68 + * We have to short-circuit the logic that adds width and height to the img tag.
69 + * It compares the URL basename, and the `file` param for each image.
70 + * This happens for any image that gets its size set non-explicitly
71 + * e.g. an image block with its size set from the sidebar to `thumbnail`).
72 + *
73 + * Optimole has a single basename for all image resizes in its URL.
74 + *
75 + * @param mixed $dimensions The dimensions of the image.
76 + * @param mixed $image_src The source of the image.
77 + * @param mixed $image_meta The meta of the image.
78 + * @param mixed $attachment_id The ID of the attachment.
79 + */
80 + public function filter_image_src_get_dimensions( $dimensions, $image_src, $image_meta, $attachment_id ) {
81 +
82 + list($width, $height) = $this->parse_dimension_from_optimized_url( $image_src );
83 +
84 + if ( false === $width || false === $height ) {
85 + return $dimensions;
86 + }
87 + $sizes = Optml_App_Replacer::image_sizes();
88 + if ( OPTML_DEBUG ) {
89 + do_action( 'optml_log', 'filter_image_src_get_dimensions: ' . $image_src . ' ' . $width . ' ' . $height . ' ' . print_r( $image_meta, true ) );
90 + }
91 + if ( $width === 'auto' && $height === 'auto' && isset( $image_meta['width'], $image_meta['height'] ) ) {
92 + return [
93 + $image_meta['width'],
94 + $image_meta['height'],
95 + ];
96 + }
97 + // If this is an image size. Return its dimensions.
98 + foreach ( $sizes as $size => $args ) {
99 + if ( (int) $args['width'] !== (int) $width ) {
100 + continue;
101 + }
102 +
103 + if ( (int) $args['height'] !== (int) $height ) {
104 + continue;
105 + }
106 +
107 + return [
108 + $args['width'],
109 + $args['height'],
110 + ];
111 + }
112 +
113 + // Fall-through with the original dimensions.
114 + return $dimensions;
62 115 }
63 116 /**
64 - * Called by hook to replace image tags in content.
117 + * Filter the attachment image attributes to add the srcset attribute with retina support.
65 118 *
66 - * @param string $content The content to process.
67 - * @param array $images List of image tags.
119 + * This is covering the case where the image has no sizes.
68 120 *
69 - * @return mixed
121 + * @param array $attr The attributes.
122 + * @param WP_Post $attachment The attachment.
123 + * @param string $size The size.
70 124 */
125 + public function filter_attachment_image_attributes( $attr, $attachment, $size ) {
126 + if ( ! isset( $attr['srcset'] ) && isset( $attr['src'] ) && strpos( $attr['src'], '/w:' ) !== false ) {
127 + $attr['srcset'] = str_replace( '/w:', '/dpr:2/w:', $attr['src'] ) . ' 2x';
128 + }
129 + return $attr;
130 + }
71 131
132 +
72 133 /**
73 134 * Replace in given content the given image tags with video tags is image is gif
74 135 *
75 136 * @param string $image_url Image url to process.
@@ -88,9 +149,10 @@
88 149 }
89 150 $link_mp4 = apply_filters(
90 151 'optml_content_url',
91 152 $image_url,
92 - ['width' => 'auto',
153 + [
154 + 'width' => 'auto',
93 155 'height' => 'auto',
94 156 'format' => 'mp4',
95 157 ]
96 158 );
@@ -97,9 +159,10 @@
97 159
98 160 $link_png = apply_filters(
99 161 'optml_content_url',
100 162 $image_url,
101 - ['width' => 'auto',
163 + [
164 + 'width' => 'auto',
102 165 'height' => 'auto',
103 166 'quality' => 'eco',
104 167 ]
105 168 );
@@ -134,8 +197,11 @@
134 197 public function process_image_tags( $content, $images = [] ) {
135 198
136 199 $image_sizes = self::image_sizes();
137 200 $sizes2crop = self::size_to_crop();
201 + if ( OPTML_DEBUG ) {
202 + do_action( 'optml_log', 'Images tags to process: ' . print_r( $images, true ) );
203 + }
138 204 foreach ( $images[0] as $index => $tag ) {
139 205 $width = $height = false;
140 206 $crop = null;
141 207 $image_tag = $images['img_tag'][ $index ];
@@ -161,15 +227,12 @@
161 227 );
162 228 $images['img_url'][ $index ] = $new_src;
163 229 }
164 230 if ( ( apply_filters( 'optml_ignore_image_link', false, $src ) ||
165 - ( false !== strpos( $src, Optml_Config::$service_url ) && ! $this->url_has_dam_flag( $src ) ) ||
166 - ! $this->can_replace_url( $src ) ||
167 - ! $this->can_replace_tag( $images['img_url'][ $index ], $tag ) ) && ! Optml_Media_Offload::is_not_processed_image( $src )
231 + ! $this->can_replace_tag( $images['img_url'][ $index ], $tag ) ) && ! Optml_Media_Offload::is_not_processed_image( $src )
168 232 ) {
169 233 continue; // @codeCoverageIgnore
170 234 }
171 -
172 235 $resize = apply_filters( 'optml_default_crop', [] );
173 236
174 237 list( $width, $height, $resize ) = self::parse_dimensions_from_tag(
175 238 $images['img_tag'][ $index ],
@@ -198,14 +261,18 @@
198 261 continue;
199 262 }
200 263
201 264 $tmp = $this->strip_image_size_from_url( $tmp );
202 - $new_url = apply_filters( 'optml_content_url', $tmp, $optml_args );
203 265
204 - if ( $new_url === $tmp && ! Optml_Media_Offload::is_not_processed_image( $src ) ) {
205 - continue; // @codeCoverageIgnore
266 + $image_id = $this->get_id_by_url( $images['img_url'][ $index ] );
267 +
268 + if ( Optml_Manager::instance()->page_profiler->get_crop_status( $image_id ) && empty( $optml_args['resize'] ) ) {
269 + $optml_args['resize'] = $this->to_optml_crop( true );
270 + $optml_args['force'] = true; // here we need to force the url to be rebuild if the image is already using an Optimole URL.
206 271 }
207 272
273 + $new_url = apply_filters( 'optml_content_url', $tmp, $optml_args );
274 +
208 275 $image_tag = str_replace(
209 276 [
210 277 'width="' . $width . '"',
211 278 'width=\"' . $width . '\"',
@@ -219,20 +286,37 @@
219 286 'height=\"' . $optml_args['height'] . '\"',
220 287 ],
221 288 $image_tag
222 289 );
290 + // If the image is in header or has a class excluded from lazyload or is an excluded gif, we need to do the regular replace.
291 + if ( $images['in_no_script'][ $index ] || $should_lazy_gif === false ) {
223 292
224 - // If the image is in header or has a class excluded from lazyload or is an excluded gif, we need to do the regular replace.
225 - if ( $images['in_header'][ $index ] || $should_lazy_gif === false ) {
226 293 $image_tag = $this->regular_tag_replace( $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag );
227 294 } else {
228 295 $image_tag = apply_filters( 'optml_tag_replace', $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag );
229 296 }
297 + if ( strpos( $image_tag, 'data-opt-id=' ) === false ) {
298 + $image_tag = preg_replace( '/<img/im', '<img data-opt-id=' . $image_id . ' ', $image_tag );
299 + }
300 +
301 + if ( $priority = Links::is_preloaded( $image_id ) ) { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.Found
302 + Links::preload_tag( $image_tag, $priority );
303 + }
304 +
230 305 if ( strpos( $image_tag, 'decoding=' ) === false ) {
231 - $pattern = '/<img(.*?)/is';
232 - $replace = '<img decoding=async $1';
233 - $image_tag = preg_replace( $pattern, $replace, $image_tag );
306 + $image_tag = str_replace( 'data-opt-id=', 'decoding=async data-opt-id=', $image_tag );
234 307 }
308 + if ( strpos( $image_tag, 'width=' ) === false || strpos( $image_tag, 'height=' ) === false ) {
309 + $missing_dimensions = Optml_Manager::instance()->page_profiler->get_missing_dimensions( $image_id );
310 + if ( ! empty( $missing_dimensions ) ) {
311 + if ( strpos( $image_tag, 'width=' ) === false ) {
312 + $image_tag = str_replace( 'data-opt-id=', 'width="' . $missing_dimensions['w'] . '" data-opt-id=', $image_tag );
313 + }
314 + if ( strpos( $image_tag, 'height=' ) === false ) {
315 + $image_tag = str_replace( 'data-opt-id=', 'height="' . $missing_dimensions['h'] . '" data-opt-id=', $image_tag );
316 + }
317 + }
318 + }
235 319 $content = str_replace( $images['img_tag'][ $index ], $image_tag, $content );
236 320 }
237 321 return $content;
238 322 }
@@ -320,10 +404,23 @@
320 404 *
321 405 * @return string
322 406 */
323 407 public function regular_tag_replace( $new_tag, $original_url, $new_url, $optml_args, $is_slashed = false, $full_tag = '' ) {
408 + if ( OPTML_DEBUG ) {
409 + do_action( 'optml_log', 'regular_tag_replace: ' . $original_url . ' ' . $new_url . $new_tag );
410 + }
324 411 $pattern = '/(?<!\/)' . preg_quote( $original_url, '/' ) . '/i';
325 412 $replace = $is_slashed ? addcslashes( $new_url, '/' ) : $new_url;
413 +
414 + // Get image ID for srcset enhancement
415 + $image_id = $this->get_id_by_url( $original_url );
416 +
417 + // Add missing srcset attributes based on measurements from JavaScript module
418 + $missing_srcsets = Optml_Manager::instance()->page_profiler->get_missing_srcsets( $image_id );
419 + if ( ! empty( $missing_srcsets ) ) {
420 + $new_tag = $this->add_missing_srcset_attributes( $new_tag, $missing_srcsets, $new_url, $is_slashed );
421 + }
422 +
326 423 if ( $this->settings->get( 'lazyload' ) === 'enabled' && $this->settings->get( 'native_lazyload' ) === 'enabled'
327 424 && apply_filters( 'optml_should_load_eager', '__return_true' ) && ! $this->is_valid_gif( $original_url ) ) {
328 425 if ( strpos( $new_tag, 'loading=' ) === false ) {
329 426 $new_tag = preg_replace( '/<img/im', $is_slashed ? '<img loading=\"eager\"' : '<img loading="eager"', $new_tag );
@@ -331,22 +428,407 @@
331 428 $new_tag = $is_slashed ? str_replace( 'loading=\"lazy\"', 'loading=\"eager\"', $new_tag ) : str_replace( 'loading="lazy"', 'loading="eager"', $new_tag );
332 429 }
333 430 }
334 431
335 - self::$lazyload_skipped_images ++;
432 + $no_viewport_data_available = true;
433 +
434 + if ( $this->settings->is_lazyload_type_viewport() ) {
435 + $image_id = $this->get_id_by_url( $original_url );
436 + $is_lcp_image = Optml_Manager::instance()->page_profiler->is_lcp_image_in_all_viewports( $image_id );
437 + $no_viewport_data_available = ! Optml_Manager::instance()->page_profiler->is_data_available();
438 + if ( OPTML_DEBUG ) {
439 + do_action( 'optml_log', 'Adding fetchpriority image is LCP ' . $original_url . '|' . $image_id );
440 + }
441 +
442 + if ( $is_lcp_image ) {
443 + $new_tag = preg_replace( '/<img/im', $is_slashed ? '<img fetchpriority=\"high\"' : '<img fetchpriority="high"', $new_tag );
444 + }
445 + }
446 +
447 + // If the image is between the first images we add the fetchpriority attribute to improve the LCP.
448 + if (
449 + $no_viewport_data_available &&
450 + $this->settings->is_lazyload_type_fixed() &&
451 + self::$lazyload_skipped_images < Optml_Lazyload_Replacer::get_skip_lazyload_limit() &&
452 + false === strpos( $new_tag, 'fetchpriority=' )
453 + ) {
454 + $new_tag = preg_replace( '/<img/im', $is_slashed ? '<img fetchpriority=\"high\"' : '<img fetchpriority="high"', $new_tag );
455 + }
456 +
457 + ++self::$lazyload_skipped_images;
336 458 return preg_replace( $pattern, $replace, $new_tag );
337 459 }
338 460
339 461 /**
462 + * Add or enhance srcset attributes to an image tag based on measurements from JavaScript module.
463 + *
464 + * @param string $tag The image tag.
465 + * @param array<array{w: int, h: int, s: string, d: int, b: int}> $missing_srcsets Array of missing srcset data from JavaScript module.
466 + * @param string $new_url The new image URL.
467 + * @param bool $is_slashed Whether the URL needs to be slashed.
468 + *
469 + * @return string The modified image tag with enhanced srcset attributes.
470 + */
471 + public function add_missing_srcset_attributes( $tag, $missing_srcsets, $new_url, $is_slashed = false ) {
472 + if ( OPTML_DEBUG ) {
473 + do_action( 'optml_log', 'add_missing_srcset_attributes: ' . $new_url . ' ' . print_r( $missing_srcsets, true ) );
474 + }
475 + if ( empty( $missing_srcsets ) || ! is_array( $missing_srcsets ) ) {
476 + return $tag;
477 + }
478 +
479 + // Check if srcset already exists
480 + $has_existing_srcset = strpos( $tag, 'srcset=' ) !== false;
481 + $has_existing_sizes = strpos( $tag, 'sizes=' ) !== false;
482 +
483 + // Build new srcset entries from missing srcset data
484 + $new_srcset_entries = [];
485 + $new_sizes_entries = [];
486 +
487 + foreach ( $missing_srcsets as $srcset_data ) {
488 + // Validate required fields
489 + if ( ! isset( $srcset_data['w'] ) || ! isset( $srcset_data['h'] ) || ! isset( $srcset_data['s'] ) ) {
490 + continue;
491 + }
492 +
493 + $width = (int) $srcset_data['w'];
494 + $height = (int) $srcset_data['h'];
495 + $descriptor = $srcset_data['s']; // e.g., "200w"
496 + $dpr = isset( $srcset_data['d'] ) ? (int) $srcset_data['d'] : 1;
497 + // If the retina images are not enabled, we don't need to add the retina srcset.
498 + if ( $dpr > 1 && $this->settings->get( 'retina_images' ) !== 'enabled' ) {
499 + continue;
500 + }
501 + $breakpoint = isset( $srcset_data['b'] ) ? (int) $srcset_data['b'] : 0;
502 +
503 + // Generate optimized URL for this size
504 + $optimized_url = $this->change_url_for_size( $new_url, $width, $height, $dpr );
505 +
506 + if ( $optimized_url ) {
507 + $escaped_url = esc_url( $optimized_url );
508 + if ( empty( $escaped_url ) ) {
509 + continue;
510 + }
511 + $new_srcset_entries[] = $escaped_url . ' ' . esc_attr( $descriptor );
512 +
513 + // Add sizes attribute entry for responsive breakpoints
514 + if ( $breakpoint > 0 ) {
515 + $new_sizes_entries[] = '(max-width: ' . $breakpoint . 'px) ' . $width . 'px';
516 + }
517 + }
518 + }
519 +
520 + if ( empty( $new_srcset_entries ) ) {
521 + return $tag;
522 + }
523 +
524 + if ( $has_existing_srcset ) {
525 + // Enhance existing srcset
526 + $tag = $this->enhance_existing_srcset( $tag, $new_srcset_entries, $is_slashed );
527 + } else {
528 + // Add new srcset attribute
529 + $srcset_value = implode( ', ', $new_srcset_entries );
530 + // Escape backreference metacharacters so this value can't be expanded by preg_replace() below.
531 + $srcset_value = addcslashes( $srcset_value, '\\$' );
532 + $srcset_attr = $is_slashed ? 'srcset=\"' . addcslashes( $srcset_value, '"' ) . '\"' : 'srcset="' . $srcset_value . '"';
533 +
534 + // Insert srcset attribute after the src attribute
535 + $tag = preg_replace( '/(src=["\'][^"\']*["\'])/i', '$1 ' . $srcset_attr, $tag );
536 + }
537 +
538 + // Handle sizes attribute - skip if existing sizes contains calc() or complex formulas
539 + if ( ! empty( $new_sizes_entries ) ) {
540 + $should_skip_sizes = false;
541 +
542 + if ( $has_existing_sizes ) {
543 + // Check if existing sizes contains calc() or complex formulas
544 + if ( preg_match( '/sizes=["\']([^"\']*)["\']/i', $tag, $matches ) ) {
545 + $existing_sizes = $matches[1];
546 + if ( $this->should_skip_sizes( $existing_sizes ) ) {
547 + $should_skip_sizes = true;
548 + if ( OPTML_DEBUG ) {
549 + do_action( 'optml_log', 'Skipping sizes enhancement due to complex formula: ' . $existing_sizes );
550 + }
551 + }
552 + }
553 + }
554 +
555 + if ( ! $should_skip_sizes ) {
556 + // Filter sizes entries to ensure they're appropriate for the container
557 + $filtered_sizes_entries = $this->filter_sizes_entries_for_container( $new_sizes_entries, $tag );
558 +
559 + if ( ! empty( $filtered_sizes_entries ) ) {
560 + if ( $has_existing_sizes ) {
561 + // Enhance existing sizes
562 + $tag = $this->enhance_existing_sizes( $tag, $filtered_sizes_entries, $is_slashed );
563 + } else {
564 + // Add new sizes attribute
565 + $sizes_entries = array_unique( $filtered_sizes_entries );
566 + // Sort ascending by breakpoint value (numeric)
567 + usort(
568 + $sizes_entries,
569 + function ( $a, $b ) {
570 + preg_match( '/max-width:\s*(\d+)/', $a, $ma );
571 + preg_match( '/max-width:\s*(\d+)/', $b, $mb );
572 + return ( $ma[1] ?? 0 ) - ( $mb[1] ?? 0 );
573 + }
574 + );
575 +
576 + $sizes_value = implode( ', ', $sizes_entries );
577 + // Escape backreference metacharacters before wrapping, see add_missing_srcset_attributes() above.
578 + $sizes_value = addcslashes( $sizes_value, '\\$' );
579 + $sizes_attr = $is_slashed ? 'sizes=\"' . addcslashes( $sizes_value, '"' ) . '\"' : 'sizes="' . $sizes_value . '"';
580 +
581 + // Insert sizes attribute after srcset
582 + $tag = preg_replace( '/(srcset=["\'][^"\']*["\'])/i', '$1 ' . $sizes_attr, $tag );
583 + }
584 + }
585 + } else {
586 + // Log that sizes were skipped due to complex formulas
587 + if ( OPTML_DEBUG ) {
588 + do_action( 'optml_log', 'Skipped adding sizes due to complex formula detection' );
589 + }
590 + }
591 + }
592 +
593 + if ( OPTML_DEBUG ) {
594 + $action = $has_existing_srcset ? 'Enhanced' : 'Added';
595 + do_action( 'optml_log', $action . ' srcset for image: ' . $new_url . ' with ' . count( $new_srcset_entries ) . ' new entries' );
596 + }
597 +
598 + return $tag;
599 + }
600 +
601 + /**
602 + * Enhance existing srcset attribute by adding new entries.
603 + *
604 + * @param string $tag The image tag.
605 + * @param array<int, string> $new_srcset_entries Array of new srcset entries to add.
606 + * @param bool $is_slashed Whether the URL needs to be slashed.
607 + *
608 + * @return string The modified image tag with enhanced srcset.
609 + */
610 + public function enhance_existing_srcset( $tag, $new_srcset_entries, $is_slashed = false ) {
611 + // Extract existing srcset value
612 + if ( preg_match( '/srcset=["\']([^"\']*)["\']/i', $tag, $matches ) ) {
613 + $existing_srcset = $matches[1];
614 + $existing_entries = array_map( 'trim', explode( ',', $existing_srcset ) );
615 +
616 + // Merge with new entries, avoiding duplicates
617 + $all_entries = array_merge( $existing_entries, $new_srcset_entries );
618 + $all_entries = array_unique( $all_entries );
619 +
620 + // Sort by descriptor (width) for better organization
621 + usort(
622 + $all_entries,
623 + function ( $a, $b ) {
624 + preg_match( '/(\d+)w/', $a, $matches_a );
625 + preg_match( '/(\d+)w/', $b, $matches_b );
626 + $width_a = isset( $matches_a[1] ) ? (int) $matches_a[1] : 0;
627 + $width_b = isset( $matches_b[1] ) ? (int) $matches_b[1] : 0;
628 + return $width_a - $width_b;
629 + }
630 + );
631 +
632 + $enhanced_srcset = implode( ', ', $all_entries );
633 + // Escape backreference metacharacters so this value can't be expanded by preg_replace() below.
634 + $enhanced_srcset = addcslashes( $enhanced_srcset, '\\$' );
635 + $srcset_attr = $is_slashed ? 'srcset=\"' . addcslashes( $enhanced_srcset, '"' ) . '\"' : 'srcset="' . $enhanced_srcset . '"';
636 +
637 + // Replace existing srcset
638 + $tag = preg_replace( '/srcset=["\'][^"\']*["\']/i', $srcset_attr, $tag );
639 + }
640 +
641 + return $tag;
642 + }
643 +
644 + /**
645 + * Enhance existing sizes attribute by adding new breakpoint entries.
646 + *
647 + * @param string $tag The image tag.
648 + * @param array<int, string> $new_sizes_entries Array of new sizes entries to add.
649 + * @param bool $is_slashed Whether the URL needs to be slashed.
650 + *
651 + * @return string The modified image tag with enhanced sizes.
652 + */
653 + public function enhance_existing_sizes( $tag, $new_sizes_entries, $is_slashed = false ) {
654 + // Extract existing sizes value
655 + if ( preg_match( '/sizes=["\']([^"\']*)["\']/i', $tag, $matches ) ) {
656 + $existing_sizes = $matches[1];
657 + $existing_entries = array_map( 'trim', explode( ',', $existing_sizes ) );
658 +
659 + // Merge with new entries, avoiding duplicates
660 + $all_entries = array_merge( $existing_entries, $new_sizes_entries );
661 + $all_entries = array_unique( $all_entries );
662 +
663 + // Sort by breakpoint (descending)
664 + usort(
665 + $all_entries,
666 + function ( $a, $b ) {
667 + preg_match( '/max-width:\s*(\d+)px/', $a, $matches_a );
668 + preg_match( '/max-width:\s*(\d+)px/', $b, $matches_b );
669 + $breakpoint_a = isset( $matches_a[1] ) ? (int) $matches_a[1] : 0;
670 + $breakpoint_b = isset( $matches_b[1] ) ? (int) $matches_b[1] : 0;
671 + return $breakpoint_b - $breakpoint_a; // Descending order
672 + }
673 + );
674 +
675 + $enhanced_sizes = implode( ', ', $all_entries );
676 + // Escape backreference metacharacters before wrapping, see enhance_existing_srcset() above.
677 + $enhanced_sizes = addcslashes( $enhanced_sizes, '\\$' );
678 + $sizes_attr = $is_slashed ? 'sizes=\"' . addcslashes( $enhanced_sizes, '"' ) . '\"' : 'sizes="' . $enhanced_sizes . '"';
679 +
680 + // Replace existing sizes
681 + $tag = preg_replace( '/sizes=["\'][^"\']*["\']/i', $sizes_attr, $tag );
682 + }
683 +
684 + return $tag;
685 + }
686 +
687 + /**
688 + * Check if sizes attribute contains complex formulas that should not be modified.
689 + *
690 + * @param string $sizes_value The sizes attribute value.
691 + *
692 + * @return bool True if contains complex formulas, false otherwise.
693 + */
694 + public function should_skip_sizes( $sizes_value ) {
695 + // Check for calc() functions
696 + if ( strpos( $sizes_value, 'calc(' ) !== false ) {
697 + return true;
698 + }
699 +
700 + // Check for complex mathematical operations
701 + if ( preg_match( '/\d+%\s*[+\-*\/]|[+\-*\/]\s*\d+%|\d+px\s*[+\-*\/]|[+\-*\/]\s*\d+px/', $sizes_value ) ) {
702 + return true;
703 + }
704 +
705 + // Check for viewport units with calculations
706 + if ( preg_match( '/\d+vw\s*[+\-*\/]|\d+vh\s*[+\-*\/]|\d+vmin\s*[+\-*\/]|\d+vmax\s*[+\-*\/]/', $sizes_value ) ) {
707 + return true;
708 + }
709 +
710 + // Check for complex CSS functions
711 + if ( preg_match( '/(min|max|clamp)\(/', $sizes_value ) ) {
712 + return true;
713 + }
714 +
715 + // Check for multiple percentage values with operations
716 + if ( preg_match( '/\d+%\s*[+\-*\/]\s*\d+%/', $sizes_value ) ) {
717 + return true;
718 + }
719 +
720 + return false;
721 + }
722 +
723 + /**
724 + * Filter sizes entries to ensure they're appropriate for the container size.
725 + * This prevents oversized images from being selected for small containers.
726 + *
727 + * @param array<int, string> $sizes_entries Array of sizes entries from JavaScript module.
728 + * @param string $tag The image tag to extract container dimensions from.
729 + *
730 + * @return array<int, string> Filtered sizes entries appropriate for the container.
731 + */
732 + public function filter_sizes_entries_for_container( $sizes_entries, $tag ) {
733 + if ( empty( $sizes_entries ) ) {
734 + return [];
735 + }
736 +
737 + // Extract container dimensions from the image tag
738 + $container_width = null;
739 + $container_height = null;
740 +
741 + // Try to get width and height from the tag
742 + if ( preg_match( '/width=["\']?(\d+)["\']?/i', $tag, $width_match ) ) {
743 + $container_width = (int) $width_match[1];
744 + }
745 + if ( preg_match( '/height=["\']?(\d+)["\']?/i', $tag, $height_match ) ) {
746 + $container_height = (int) $height_match[1];
747 + }
748 +
749 + // If we can't determine container size, return all entries (fallback)
750 + if ( ! $container_width || ! $container_height ) {
751 + if ( OPTML_DEBUG ) {
752 + do_action( 'optml_log', 'Cannot determine container size, using all sizes entries' );
753 + }
754 + return $sizes_entries;
755 + }
756 +
757 + // Calculate maximum reasonable size for this container
758 + $max_reasonable_width = $container_width * 1.5; // Allow up to 1.5x the container size
759 +
760 + $filtered_entries = [];
761 +
762 + foreach ( $sizes_entries as $entry ) {
763 + // Extract image size from the entry (e.g., "(max-width: 768px) 400px" -> 400)
764 + if ( preg_match( '/\(max-width:\s*\d+px\)\s*(\d+)px/', $entry, $matches ) ) {
765 + $image_width = (int) $matches[1];
766 +
767 + // Only include entries where the image size is reasonable for the container
768 + if ( $image_width <= $max_reasonable_width ) {
769 + $filtered_entries[] = $entry;
770 + } else {
771 + if ( OPTML_DEBUG ) {
772 + do_action( 'optml_log', "Filtered out oversized entry: {$entry} (container: {$container_width}px, image: {$image_width}px)" );
773 + }
774 + }
775 + } else {
776 + // If we can't parse the entry, include it (better safe than sorry)
777 + $filtered_entries[] = $entry;
778 + }
779 + }
780 +
781 + if ( OPTML_DEBUG ) {
782 + do_action( 'optml_log', "Filtered sizes entries for container {$container_width}x{$container_height}: " . count( $filtered_entries ) . ' of ' . count( $sizes_entries ) . ' entries' );
783 + }
784 +
785 + return $filtered_entries;
786 + }
787 +
788 + /**
789 + * Generate optimized URL for a specific size and DPR.
790 + *
791 + * @param string $original_url The original image URL.
792 + * @param int $width The target width.
793 + * @param int $height The target height.
794 + * @param int $dpr The device pixel ratio.
795 + *
796 + * @return string The optimized URL or false on failure.
797 + */
798 + public function change_url_for_size( $original_url, $width, $height, $dpr = 1 ) {
799 + // Assume w and h are always present - just replace them
800 + // Updated regex to match w:auto, w:123, h:auto, h:456, etc.
801 + $replacements = [
802 + '/w:(?:auto|\d+)/' => 'w:' . $width,
803 + '/h:(?:auto|\d+)/' => 'h:' . $height,
804 + ];
805 +
806 + // Handle DPR - replace if exists, add after w: if missing
807 + if ( $dpr > 1 ) {
808 + if ( strpos( $original_url, 'dpr:' ) !== false ) {
809 + $replacements['/dpr:\d+/'] = 'dpr:' . $dpr;
810 + } else {
811 + // Add DPR after width parameter - use the same pattern as width replacement
812 + $replacements['/(w:(?:auto|\d+))/'] = '$1/dpr:' . $dpr;
813 + }
814 + } else {
815 + $replacements['/dpr:\d+\//'] = '';
816 + }
817 +
818 + return preg_replace( array_keys( $replacements ), array_values( $replacements ), $original_url );
819 + }
820 +
821 + /**
340 822 * Replace image URLs in the srcset attributes and in case there is a resize in action, also replace the sizes.
341 823 *
342 - * @param array $sources Array of image sources.
343 - * @param array $size_array Array of width and height values in pixels (in that order).
344 - * @param string $image_src The 'src' of the image.
345 - * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
346 - * @param int $attachment_id Image attachment ID.
824 + * @param array<int, array{url: string, descriptor: string, value: int}>|mixed $sources Array of image sources.
825 + * @param array{0: int, 1: int}|int[] $size_array Array of width and height values in pixels (in that order).
826 + * @param string $image_src The 'src' of the image.
827 + * @param array<string, mixed> $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
828 + * @param int $attachment_id Image attachment ID or 0.
347 829 *
348 - * @return array
830 + * @return array|mixed
349 831 */
350 832 public function filter_srcset_attr( $sources = [], $size_array = [], $image_src = '', $image_meta = [], $attachment_id = 0 ) {
351 833 if ( ! is_array( $sources ) ) {
352 834 return $sources;
@@ -359,9 +841,13 @@
359 841 if ( count( $size_array ) === 2 ) {
360 842 $sizes = self::size_to_crop();
361 843 $cropping = isset( $sizes[ $size_array[0] . $size_array[1] ] ) ? $this->to_optml_crop( $sizes[ $size_array[0] . $size_array[1] ] ) : null;
362 844 }
363 -
845 + if ( OPTML_DEBUG ) {
846 + do_action( 'optml_log', 'sources: ' . print_r( $sources, true ) . ' size_array: ' . print_r( $size_array, true ) );
847 + }
848 + $biggest_size = [];
849 + $max_width = 0;
364 850 foreach ( $sources as $i => $source ) {
365 851 $url = $source['url'];
366 852 if ( Optml_Media_Offload::is_uploaded_image( $url ) ) {
367 853 continue;
@@ -396,11 +882,21 @@
396 882 $args['resize'] = $cropping;
397 883 } else {
398 884 $args['resize'] = $this->to_optml_crop( $file_crop );
399 885 }
886 + if ( $i > $max_width ) {
887 + $max_width = $i;
888 + $biggest_size = [ $args, $original_url ];
889 + }
400 890 $sources[ $i ]['url'] = apply_filters( 'optml_content_url', $original_url, $args );
891 +
401 892 }
402 893
894 + if ( $this->settings->get( 'retina_images' ) === 'enabled' && ! empty( $biggest_size ) ) {
895 + $sources[ $max_width * 2 ]['url'] = apply_filters( 'optml_content_url', $biggest_size[1], array_merge( $biggest_size[0], [ 'dpr' => 2 ] ) );
896 + $sources[ $max_width * 2 ]['value'] = 2;
897 + $sources[ $max_width * 2 ]['descriptor'] = 'x';
898 + }
403 899 return $sources;
404 900 }
405 901
406 902 /**
@@ -405,12 +901,12 @@
405 901
406 902 /**
407 903 * Filters sizes attribute of the images.
408 904 *
409 - * @param array $sizes An array of media query breakpoints.
410 - * @param array $size Width and height of the image.
905 + * @param string $sizes An array of media query breakpoints.
906 + * @param string|int[] $size Width and height of the image.
411 907 *
412 - * @return mixed An array of media query breakpoints.
908 + * @return string An array of media query breakpoints.
413 909 */
414 910 public function filter_sizes_attr( $sizes, $size ) {
415 911
416 912 if ( ! doing_filter( 'the_content' ) ) {
@@ -442,8 +938,11 @@
442 938 * @return array
443 939 */
444 940 public function filter_image_downsize( $image, $attachment_id, $size ) {
445 941
942 + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
943 + return $image;
944 + }
446 945 $image_url = wp_get_attachment_url( $attachment_id );
447 946 if ( Optml_Media_Offload::is_uploaded_image( $image_url ) ) {
448 947 return $image;
449 948 }
@@ -451,47 +950,10 @@
451 950 return $image;
452 951 }
453 952
454 953 $image_meta = wp_get_attachment_metadata( $attachment_id );
455 - $image_args = self::image_sizes();
456 - // default size
457 - $sizes = [
458 - 'width' => isset( $image_meta['width'] ) ? intval( $image_meta['width'] ) : false,
459 - 'height' => isset( $image_meta['height'] ) ? intval( $image_meta['height'] ) : false,
460 - ];
954 + $sizes = $this->size_to_dimension( $size, $image_meta, $attachment_id );
461 955
462 - switch ( $size ) {
463 - case is_array( $size ):
464 - $width = isset( $size[0] ) ? (int) $size[0] : false;
465 - $height = isset( $size[1] ) ? (int) $size[1] : false;
466 - if ( ! $width || ! $height ) {
467 - break;
468 - }
469 - $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $width, $height );
470 - if ( $image_resized ) {
471 - $width = $image_resized[6];
472 - $height = $image_resized[7];
473 - } else {
474 - $width = $image_meta['width'];
475 - $height = $image_meta['height'];
476 - }
477 - list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $width, $height, $size );
478 -
479 - break;
480 - case 'full' !== $size && isset( $image_args[ $size ] ):
481 - $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $image_args[ $size ]['width'], $image_args[ $size ]['height'], $image_args[ $size ]['crop'] );
482 -
483 - if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
484 - $sizes['width'] = $image_resized[6];
485 - $sizes['height'] = $image_resized[7];
486 - }
487 -
488 - list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $sizes['width'], $sizes['height'], $size, 'display' );
489 -
490 - $sizes['resize'] = $this->to_optml_crop( $image_args[ $size ]['crop'] );
491 -
492 - break;
493 - }
494 956 $image_url = $this->strip_image_size_from_url( $image_url );
495 957
496 958 $new_url = apply_filters( 'optml_content_url', $image_url, $sizes );
497 959
@@ -534,6 +996,5 @@
534 996 public function __wakeup() {
535 997 // Unserializing instances of the class is forbidden.
536 998 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
537 999 }
538 -
539 1000 }