PluginProbe
Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution / trunk
Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution vtrunk
2.0.1 trunk 1.0.0 1.1.0 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 1.4.2 1.5.0 1.6.0 1.6.1 1.7.0 1.7.1 1.8.0 1.9.0 2.0.0
solid-performance / src / Performance / Dom / Modifiers / Image_Transformation_Modifier.php

Image_Transformation_Modifier.php in Solid Performance – Your No-Code Caching, Performance, & Page Speed Solution trunk, at src/Performance/Dom/Modifiers/Image_Transformation_Modifier.php

249 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The Image Transformation DOM Modifier.
4 *
5 * @package SolidWP\Performance
6 */
7
8 declare( strict_types=1 );
9
10 namespace SolidWP\Performance\Dom\Modifiers;
11
12 use Closure;
13 use InvalidArgumentException;
14 use LogicException;
15 use SolidWP\Performance\Config\Config;
16 use SolidWP\Performance\Dom\Contracts\Modifier;
17 use SolidWP\Performance\Image_Transformation\Image;
18 use SolidWP\Performance\Psr\Log\LoggerInterface;
19 use SolidWP\Performance\Symfony\Component\DomCrawler\Crawler;
20
21 /**
22 * Replaces inline style background URLs with their transformed version.
23 *
24 * @package SolidWP\Performance
25 */
26 final class Image_Transformation_Modifier implements Modifier {
27
28 /**
29 * @var Config
30 */
31 private Config $config;
32
33 /**
34 * @var LoggerInterface
35 */
36 private LoggerInterface $logger;
37
38 /**
39 * @var Image
40 */
41 private Image $image;
42
43 /**
44 * @param Config $config The config object.
45 * @param LoggerInterface $logger The logger.
46 * @param Image $image The image object.
47 */
48 public function __construct(
49 Config $config,
50 LoggerInterface $logger,
51 Image $image
52 ) {
53 $this->config = $config;
54 $this->logger = $logger;
55 $this->image = $image;
56 }
57
58 /**
59 * Whether this pipeline step is enabled.
60 *
61 * @return bool
62 */
63 public function is_enabled(): bool {
64 return (bool) $this->config->get( 'page_cache.image_transformation.enabled' );
65 }
66
67 /**
68 * Return true immediately if we're enabled, otherwise pass the current result to the next
69 * stage.
70 *
71 * @param bool $enabled Whether the previous stage is enabled.
72 * @param Closure $next The closer passed to the next stage.
73 *
74 * @return bool
75 */
76 public function enabled( bool $enabled, Closure $next ): bool {
77 return $this->is_enabled() ?: $next( $enabled );
78 }
79
80 /**
81 * Find elements that have a style tag which contains a `url()` data tag and attempt to
82 * replace the image URL with our transformed image URl.
83 *
84 * @param Crawler $crawler The Symfony Crawler instance.
85 * @param Closure $next The next closure to pass in the pipeline.
86 *
87 * @throws InvalidArgumentException When current node is empty.
88 * @throws LogicException If the CssSelector Component is not available.
89 *
90 * @return Crawler
91 */
92 public function modify( Crawler $crawler, Closure $next ): Crawler {
93 if ( ! $this->is_enabled() ) {
94
95 $this->logger->debug( 'Image_Transformation_Modifier is marked as disabled. Skipping...' );
96
97 return $next( $crawler );
98 }
99
100 // Iterate through all style elements.
101 $crawler->filter( 'style' )->each(
102 function ( Crawler $node ) {
103 $style = $node->text( '', false );
104
105 // Match CSS property:value pairs with background or background-image.
106 if ( ! preg_match_all( '/(background|background-image)\s*:\s*(?<values_with_urls>[^;]*url\([^)]*\)[^;]*)\s*;?/', $style, $matches ) ) {
107 return;
108 }
109
110 $values_with_urls = $matches['values_with_urls'];
111
112 $update_style = false;
113
114 foreach ( $values_with_urls as $property_value ) {
115 // Extract URLs from between all `url()` strings that start with http or https.
116 if ( ! preg_match_all( '/url\s*\(\s*(["\']?)(https?:\/\/[^ ]*?)\1\s*\)/i', $property_value, $url_matches ) ) {
117 continue;
118 }
119
120 $urls = $url_matches[2];
121
122 foreach ( $urls as $url ) {
123 $attachment_id = $this->image->url_to_id( $url );
124
125 if ( $attachment_id === 0 ) {
126 $this->logger->warning(
127 'Unable to find attachment_id for CSS style background image: {image_url}',
128 [
129 'image_url' => $url,
130 'style' => $style,
131 ]
132 );
133
134 continue;
135 }
136
137 $size = $this->image->detect_size( $url, $attachment_id );
138 $image_data = image_downsize( $attachment_id, $size );
139
140 if ( ! $image_data ) {
141 $this->logger->warning(
142 'Unable to find CSS background image_downsize data',
143 [
144 'attachment_id' => $attachment_id,
145 'image_url' => $url,
146 ]
147 );
148
149 continue;
150 }
151
152 $new_image_url = $image_data[0] ?? '';
153
154 // Replace the old URL with the transformed URL.
155 $style = str_replace( $url, $new_image_url, $style );
156
157 $update_style = true;
158 }
159 }
160
161 if ( ! $update_style ) {
162 return;
163 }
164
165 $current = $node->getNode( 0 );
166
167 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
168 $current->nodeValue = $style;
169 }
170 );
171
172 // Iterate through all elements with a style tag that contains a url() data type.
173 $crawler->filter( '[style*="url("]' )->each(
174 function ( Crawler $node ) {
175 $style = (string) $node->attr( 'style' );
176
177 // Extract a background image URL that starts with http or https.
178 if ( ! preg_match( '/url\s*\(\s*(["\']?)(https?:\/\/.*?)\1\s*\)/i', $style, $image_urls ) ) {
179 return;
180 }
181
182 $original_url = $image_urls[2] ?? '';
183
184 $classes = (string) $node->attr( 'class' );
185
186 // Extract attachment ID, try to avoid a database query.
187 if ( preg_match( '/wp-image-(\d+)/', $classes, $attachment_ids ) ) {
188 $attachment_id = (int) $attachment_ids[1];
189 } elseif ( preg_match( '/attachment-(\d+)/', $classes, $attachment_ids ) ) {
190 $attachment_id = (int) $attachment_ids[1];
191 } elseif ( $node->attr( 'data-id' ) ) {
192 $attachment_id = (int) $node->attr( 'data-id' );
193 } else {
194 $attachment_id = $this->image->url_to_id( $original_url );
195 }
196
197 if ( $attachment_id === 0 ) {
198 $this->logger->warning(
199 'Unable to find attachment_id for {image_url}',
200 [
201 'image_url' => $original_url,
202 ]
203 );
204
205 return;
206 }
207
208 // Extract thumbnail size or try to detect it from the URL.
209 $size = preg_match( '/size-([\w-]+)/', $classes, $sizes )
210 ? $sizes[1]
211 : $this->image->detect_size( $original_url, $attachment_id );
212
213 $image_data = image_downsize( $attachment_id, $size );
214
215 if ( ! $image_data ) {
216 $this->logger->warning(
217 'Unable to find image_downsize data',
218 [
219 'attachment_id' => $attachment_id,
220 'image_url' => $original_url,
221 ]
222 );
223
224 return;
225 }
226
227 $new_image_url = $image_data[0] ?? '';
228
229 // Replace the old URL with the transformed URL.
230 $style = str_replace( $original_url, $new_image_url, $style );
231
232 $this->logger->debug(
233 'Image URL Transformed',
234 [
235 'original_image_url' => $original_url,
236 'new_image_url' => $new_image_url,
237 'style' => $style,
238 ]
239 );
240
241 $current = $node->getNode( 0 );
242 $current->setAttribute( 'style', $style );
243 }
244 );
245
246 return $next( $crawler );
247 }
248 }
249