PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.13
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.13
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / media-optimization / class-dimension-injector.php

class-dimension-injector.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.13, at media-optimization/class-dimension-injector.php

218 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Metasync_Dimension_Injector
4 * Scans HTML for <img> tags missing width/height and injects them
5 * based on file metadata to prevent Cumulative Layout Shift (CLS).
6 *
7 * @package Search Atlas SEO
8 * @copyright Copyright (C) 2021-2025, Search Atlas Group - support@searchatlas.com
9 * @since 2.6.0
10 */
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 class Metasync_Dimension_Injector {
17
18 /** In-memory cache to avoid repeated file lookups within a request. */
19 private array $cache = [];
20
21 public function __construct() {
22 add_filter('the_content', [$this, 'inject_dimensions'], 30);
23 add_filter('post_thumbnail_html', [$this, 'inject_dimensions'], 30);
24 add_filter('widget_text', [$this, 'inject_dimensions'], 30);
25 }
26
27 /**
28 * Find <img> tags missing width or height and inject dimensions.
29 */
30 public function inject_dimensions(string $content): string {
31 if (empty($content) || is_admin()) {
32 return $content;
33 }
34
35 return preg_replace_callback('/<img\s[^>]+>/i', function ($matches) {
36 $tag = $matches[0];
37
38 $has_width = preg_match('/\swidth\s*=/i', $tag);
39 $has_height = preg_match('/\sheight\s*=/i', $tag);
40
41 // Both already present - nothing to do
42 if ($has_width && $has_height) {
43 return $tag;
44 }
45
46 $dims = $this->get_dimensions($tag);
47 if (!$dims) {
48 return $tag;
49 }
50
51 if (!$has_width) {
52 $tag = $this->add_attribute($tag, 'width', (string) $dims['width']);
53 }
54 if (!$has_height) {
55 $tag = $this->add_attribute($tag, 'height', (string) $dims['height']);
56 }
57
58 return $tag;
59 }, $content);
60 }
61
62 /**
63 * Try to determine image dimensions from multiple sources.
64 */
65 private function get_dimensions(string $img_tag): ?array {
66 if (!preg_match('/src=["\']([^"\']+)["\']/i', $img_tag, $m)) {
67 return null;
68 }
69 $src = $m[1];
70
71 if (isset($this->cache[$src])) {
72 return $this->cache[$src];
73 }
74
75 $dims = null;
76
77 // Strategy 1: Try to find WordPress attachment by URL
78 $dims = $this->get_dims_from_attachment($src);
79
80 // Strategy 2: Try to read the local file directly
81 if (!$dims) {
82 $dims = $this->get_dims_from_local_file($src);
83 }
84
85 // Strategy 3: For external images, try getimagesize with URL (slower)
86 if (!$dims) {
87 $dims = $this->get_dims_from_remote($src);
88 }
89
90 if ($dims) {
91 $this->cache[$src] = $dims;
92 }
93
94 return $dims;
95 }
96
97 /**
98 * Look up dimensions from WP attachment metadata.
99 */
100 private function get_dims_from_attachment(string $url): ?array {
101 $attachment_id = attachment_url_to_postid($url);
102
103 if (!$attachment_id) {
104 // Try without size suffix (e.g., image-300x200.jpg -> image.jpg)
105 $clean_url = preg_replace('/-\d+x\d+(\.\w+)$/', '$1', $url);
106 $attachment_id = attachment_url_to_postid($clean_url);
107 }
108
109 if (!$attachment_id) {
110 return null;
111 }
112
113 $meta = wp_get_attachment_metadata($attachment_id);
114 if (!$meta) {
115 return null;
116 }
117
118 // Check sub-sizes first
119 if (!empty($meta['sizes'])) {
120 $filename = wp_basename($url);
121 foreach ($meta['sizes'] as $size_data) {
122 if ($size_data['file'] === $filename) {
123 return [
124 'width' => (int) $size_data['width'],
125 'height' => (int) $size_data['height'],
126 ];
127 }
128 }
129 }
130
131 // Fall back to full size
132 if (!empty($meta['width']) && !empty($meta['height'])) {
133 return [
134 'width' => (int) $meta['width'],
135 'height' => (int) $meta['height'],
136 ];
137 }
138
139 return null;
140 }
141
142 /**
143 * Convert URL to local path and use getimagesize().
144 */
145 private function get_dims_from_local_file(string $url): ?array {
146 $upload_dir = wp_get_upload_dir();
147
148 if (strpos($url, $upload_dir['baseurl']) === false) {
149 return null;
150 }
151
152 $relative = str_replace($upload_dir['baseurl'], '', $url);
153 $file = $upload_dir['basedir'] . $relative;
154
155 if (!file_exists($file)) {
156 return null;
157 }
158
159 $info = @getimagesize($file);
160 if ($info && $info[0] > 0 && $info[1] > 0) {
161 return ['width' => $info[0], 'height' => $info[1]];
162 }
163
164 return null;
165 }
166
167 /**
168 * Fetch dimensions from a remote URL.
169 * Downloads just enough bytes to read image headers.
170 */
171 private function get_dims_from_remote(string $url): ?array {
172 if (strpos($url, 'http') !== 0) {
173 return null;
174 }
175
176 $response = wp_remote_get($url, [
177 'timeout' => 3,
178 'headers' => ['Range' => 'bytes=0-32767'],
179 ]);
180
181 if (is_wp_error($response)) {
182 return null;
183 }
184
185 $body = wp_remote_retrieve_body($response);
186 if (empty($body)) {
187 return null;
188 }
189
190 if (!function_exists('wp_tempnam')) {
191 require_once ABSPATH . 'wp-admin/includes/file.php';
192 }
193
194 $tmp = wp_tempnam($url);
195 file_put_contents($tmp, $body);
196 $info = @getimagesize($tmp);
197 @unlink($tmp);
198
199 if ($info && $info[0] > 0 && $info[1] > 0) {
200 return ['width' => $info[0], 'height' => $info[1]];
201 }
202
203 return null;
204 }
205
206 /**
207 * Insert an attribute into an <img> tag.
208 */
209 private function add_attribute(string $tag, string $name, string $value): string {
210 return preg_replace(
211 '/(<img\s)/i',
212 sprintf('$1%s="%s" ', esc_attr($name), esc_attr($value)),
213 $tag,
214 1
215 );
216 }
217 }
218