PluginProbe ʕ •ᴥ•ʔ
TinyPNG – JPEG, PNG & WebP image compression / 3.6.2
TinyPNG – JPEG, PNG & WebP image compression v3.6.2
3.7.0 3.6.14 trunk 1.0.0 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.4.0 1.5.0 1.6.0 1.7.0 1.7.1 1.7.2 2.0.0 2.0.1 2.0.2 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 3.0.0 3.0.1 3.1.0 3.2.0 3.2.1 3.3 3.4 3.4.1 3.4.2 3.4.4 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.6.0 3.6.1 3.6.10 3.6.11 3.6.12 3.6.13 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.6.9
tiny-compress-images / src / class-tiny-picture.php
tiny-compress-images / src Last commit date
compatibility 1 year ago config 4 years ago css 1 year ago data 4 years ago images 4 years ago js 1 year ago vendor 1 year ago views 10 months ago class-tiny-bulk-optimization.php 1 year ago class-tiny-cli.php 10 months ago class-tiny-compress-client.php 1 year ago class-tiny-compress-fopen.php 1 year ago class-tiny-compress.php 1 year ago class-tiny-exception.php 4 years ago class-tiny-helpers.php 1 year ago class-tiny-image-size.php 1 year ago class-tiny-image.php 1 year ago class-tiny-notices.php 10 months ago class-tiny-php.php 4 years ago class-tiny-picture.php 10 months ago class-tiny-plugin.php 10 months ago class-tiny-settings.php 10 months ago class-tiny-wp-base.php 10 months ago
class-tiny-picture.php
465 lines
1 <?php
2
3 /*
4 * Tiny Compress Images - WordPress plugin.
5 * Copyright (C) 2015-2018 Tinify B.V.
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the Free
9 * Software Foundation; either version 2 of the License, or (at your option)
10 * any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 * more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc., 51
19 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 */
21
22 /**
23 * Class responsible for parsing and modifying html to insert picture elements.
24 *
25 * 1) searches for <picture> elements or <img> elements
26 * 2) checks wether existing source has a modern alternative
27 * 3) augments or creates a picture element
28 * 4) replaces the original source with the source which includes the modern format
29 */
30 class Tiny_Picture extends Tiny_WP_Base {
31
32
33
34 /** @var string */
35 private $base_dir;
36
37 /** @var array */
38 private $allowed_domains = array();
39
40 /**
41 * Initialize the plugin.
42 *
43 * @param string $base_dir Absolute path (e.g. ABSPATH)
44 * @param array $domains List of allowed domain URLs
45 */
46 function __construct( $base_dir = ABSPATH, $domains = array() ) {
47 $this->base_dir = $base_dir;
48 $this->allowed_domains = $domains;
49
50 if ( is_admin() || is_customize_preview() ) {
51 return;
52 }
53
54 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
55 return;
56 }
57
58 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
59 return;
60 }
61
62 add_action('template_redirect', function () {
63 ob_start( array( $this, 'replace_sources' ), 1000 );
64 });
65 }
66
67 public function replace_sources( $content ) {
68 $content = $this->replace_picture_sources( $content );
69 $content = $this->replace_img_sources( $content );
70
71 return $content;
72 }
73
74 /**
75 * Will extend existing picture elements with additional sourcesets
76 *
77 * @param string $content
78 * @return string the new source html
79 */
80 private function replace_picture_sources( $content ) {
81 $picture_sources = $this->filter_pictures( $content );
82 foreach ( $picture_sources as $picture_source ) {
83 $content = $this->replace_picture( $content, $picture_source );
84 }
85 return $content;
86 }
87
88 private function replace_img_sources( $content ) {
89 $image_sources = $this->filter_images( $content );
90 foreach ( $image_sources as $image_source ) {
91 $content = Tiny_Picture::replace_image( $content, $image_source );
92 }
93 return $content;
94 }
95
96 /**
97 * Will search for all picture elements within the given source html
98 *
99 * @param string $content
100 * @return array<Tiny_Picture_Source> an array of picture element sources
101 */
102 private function filter_pictures( $content ) {
103 $matches = array();
104 if ( ! preg_match_all(
105 '#<picture\b[^>]*>.*?<\/picture>#is',
106 $content,
107 $matches
108 ) ) {
109 return array();
110 }
111
112 $pictures = array();
113 foreach ( $matches[0] as $raw_picture ) {
114 $pictures[] = new Tiny_Picture_Source(
115 $raw_picture,
116 $this->base_dir,
117 $this->allowed_domains
118 );
119 }
120
121 return $pictures;
122 }
123
124 /**
125 * Will add additional sourcesets to picture elements.
126 *
127 * @param string $content the full page content
128 * @param Tiny_Picture_Source $source the picture element
129 *
130 * @return string the updated content including augmented picture elements
131 */
132 private function replace_picture( $content, $source ) {
133 $content = str_replace( $source->raw_html, $source->augment_picture_element(), $content );
134 return $content;
135 }
136
137
138 /**
139 * Will replace img elements with picture elements that (possibly) have additional formats.
140 *
141 * @param string $content the full page content
142 * @param Tiny_Image_Source $source the picture element
143 *
144 * @return string the updated content including augmented picture elements
145 */
146 private static function replace_image( $content, $source ) {
147 $content = str_replace( $source->raw_html, $source->create_picture_elements(), $content );
148 return $content;
149 }
150
151 /**
152 * Filters out all images from the content and returns them as an array.
153 *
154 * @return Tiny_Image[]
155 */
156 private function filter_images( $content ) {
157 // Extract only the <body>...</body> section.
158 if ( preg_match( '/(?=<body).*<\/body>/is', $content, $body ) ) {
159 $content = $body[0];
160 }
161
162 // strip HTML comments.
163 $content = preg_replace( '/<!--(.*)-->/Uis', '', $content );
164
165 // strip existing <picture> blocks to avoid double-processing.
166 $content = preg_replace( '/<picture\b.*?>.*?<\/picture>/is', '', $content );
167
168 // Strip <noscript> blocks to avoid altering their contents.
169 $content = preg_replace( '/<noscript\b.*?>.*?<\/noscript>/is', '', $content );
170
171 // Find all <img> tags with any attributes.
172 if ( ! preg_match_all( '/<img\b[^>]*>/is', $content, $matches ) ) {
173 return array();
174 }
175
176 $images = array();
177 foreach ( $matches[0] as $img ) {
178 $images[] = new Tiny_Image_Source(
179 $img,
180 $this->base_dir,
181 $this->allowed_domains
182 );
183 }
184
185 return $images;
186 }
187 }
188
189 abstract class Tiny_Source_Base {
190
191
192 public $raw_html;
193 protected $base_dir;
194 protected $allowed_domains;
195 protected $valid_mimetypes;
196
197 public function __construct( $html, $base_dir, $domains ) {
198 $this->raw_html = $html;
199 $this->base_dir = $base_dir;
200 $this->allowed_domains = $domains;
201 $this->valid_mimetypes = array( 'image/webp', 'image/avif' );
202 }
203
204 protected static function get_attribute_value( $element, $name ) {
205 // Match the exact attribute name (not part of data-media, mediaType, etc.)
206 // and capture a single- or double-quoted value.
207 $delim = '~';
208 $attr = preg_quote( $name, $delim );
209 $regex = $delim . '(?<![\w:-])' . $attr . '\s*=\s*(["\'])(.*?)\1' . $delim . 'is';
210
211 if ( preg_match( $regex, $element, $m ) ) {
212 return $m[2];
213 }
214 return null;
215 }
216
217 /**
218 * Extract elements by tag name from an HTML string (regex-based).
219 *
220 * @param string $html The HTML string to search in.
221 * @param string $tagname The tag name (e.g., 'div', 'source', 'img').
222 * @return array Array of matched elements as strings.
223 */
224 protected function get_element_by_tag( $html, $tagname ) {
225 $results = [];
226
227 // Self-closing / void tag (e.g. <source />, <img />, <br />)
228 if ( preg_match_all(
229 '~<' . preg_quote( $tagname, '~' ) . '\b(?:[^>"\']+|"[^"]*"|\'[^\']*\')*/?>~i',
230 $html,
231 $matches
232 ) ) {
233 $results = array_merge( $results, $matches[0] );
234 }
235
236 // Normal paired tags (e.g. <div>…</div>)
237 $regex_tag = preg_quote( $tagname, '~' );
238 if ( preg_match_all(
239 '~<' . $regex_tag .
240 '\b(?:[^>"\']+|"[^"]*"|\'[^\']*\')*>.*?</' .
241 $regex_tag .
242 '>~is',
243 $html,
244 $matches
245 ) ) {
246 $results = array_merge( $results, $matches[0] );
247 }
248
249 return $results;
250 }
251
252 protected function get_local_path( $url ) {
253 if ( strpos( $url, 'http' ) === 0 ) {
254 $matched_domain = null;
255
256 foreach ( $this->allowed_domains as $domain ) {
257 if ( strpos( $url, $domain ) === 0 ) {
258 $matched_domain = $domain;
259 break;
260 }
261 }
262
263 if ( null === $matched_domain ) {
264 return '';
265 }
266
267 $url = substr( $url, strlen( $matched_domain ) );
268 }
269 $url = $this->base_dir . $url;
270
271 return $url;
272 }
273
274 protected function get_formatted_source( $image_source_data, $mimetype ) {
275 $format_url = Tiny_Helpers::replace_file_extension( $mimetype, $image_source_data['path'] );
276 $local_path = $this->get_local_path( $format_url );
277 if ( empty( $local_path ) ) {
278 return null;
279 }
280
281 $exists_local = file_exists( $local_path );
282 if ( $exists_local ) {
283 return array(
284 'src' => $format_url,
285 'size' => $image_source_data['size'],
286 'type' => $mimetype,
287 );
288 }
289 return null;
290 }
291
292 /**
293 * Retrieves the sources from the <img> or <source> element
294 *
295 * @return array{path: string, size: string}[] The image sources
296 */
297 protected function get_image_srcsets( $html ) {
298 $result = array();
299 $srcset = $this::get_attribute_value( $html, 'srcset' );
300
301 if ( $srcset ) {
302 // Split the srcset to get individual entries
303 $srcset_entries = explode( ',', $srcset );
304
305 foreach ( $srcset_entries as $entry ) {
306 // Trim whitespace
307 $entry = trim( $entry );
308
309 // Split by whitespace to separate path and size descriptor
310 $parts = preg_split( '/\s+/', $entry, 2 );
311
312 if ( count( $parts ) === 2 ) {
313 // We have both path and size
314 $result[] = array(
315 'path' => $parts[0],
316 'size' => $parts[1],
317 );
318 } elseif ( count( $parts ) === 1 ) {
319 // We only have a path (unusual in srcset)
320 $result[] = array(
321 'path' => $parts[0],
322 'size' => '',
323 );
324 }
325 }
326 }
327
328 $source = $this::get_attribute_value( $html, 'src' );
329 if ( ! empty( $source ) ) {
330 // No srcset, but we have a src attribute
331 $result[] = array(
332 'path' => $source,
333 'size' => '',
334 );
335 }
336 return $result;
337 }
338
339
340 /**
341 * Creates one or more <source> elements if alternative formats
342 * are available.
343 *
344 * @param string $original_source_html, either <source> or <img>
345 * @return array{string} array of <source> html
346 */
347 protected function create_alternative_sources( $original_source_html ) {
348 $srcsets = $this->get_image_srcsets( $original_source_html );
349 if ( empty( $srcsets ) ) {
350 return array();
351 }
352
353 $is_source_tag = (bool) preg_match( '#<source\b#i', $original_source_html );
354
355 $sources = array();
356 foreach ( $this->valid_mimetypes as $mimetype ) {
357 $srcset_parts = [];
358
359 foreach ( $srcsets as $srcset ) {
360 $alt_source = $this->get_formatted_source( $srcset, $mimetype );
361 if ( $alt_source ) {
362 $srcset_parts[] = trim( $alt_source['src'] . ' ' . $alt_source['size'] );
363 }
364 }
365
366 if ( ! empty( $srcset_parts ) ) {
367 $source_attr_parts = array();
368
369 $srcset_attr = implode( ', ', $srcset_parts );
370 $source_attr_parts['srcset'] = $srcset_attr;
371
372 if ( $is_source_tag ) {
373 foreach ( array( 'sizes', 'media', 'width', 'height' ) as $attr ) {
374 $attr_value = $this->get_attribute_value( $original_source_html, $attr );
375 if ( $attr_value ) {
376 $source_attr_parts[ $attr ] = $attr_value;
377 }
378 }
379 }
380
381 $source_attr_parts['type'] = $mimetype;
382 $source_parts = array( '<source' );
383 foreach ( $source_attr_parts as $source_attr_name => $source_attr_val ) {
384 $source_parts[] = $source_attr_name . '="' . $source_attr_val . '"';
385 }
386 $source_parts[] = '/>';
387 $sources[] = implode( ' ', $source_parts );
388 }
389 }
390
391 return $sources;
392 }
393 }
394
395 class Tiny_Picture_Source extends Tiny_Source_Base {
396
397
398
399 /**
400 * Adds alternative format sources (e.g., image/webp, image/avif) to an existing
401 * <picture> element based on locally available converted files.
402 *
403 * @return string The augmented <picture> HTML or the original if no additions.
404 */
405 public function augment_picture_element() {
406 $modified_sources = array();
407
408 // handle existing sources
409 $optimized_types = [ 'image/webp', 'image/avif' ];
410
411 foreach ( $this->get_element_by_tag( $this->raw_html, 'source' ) as $source_tag_html ) {
412 $type_attr = self::get_attribute_value( $source_tag_html, 'type' );
413 $type_attr = null !== $type_attr ? strtolower( trim( $type_attr ) ) : '';
414
415 // Skip if already optimized.
416 if ( '' !== $type_attr && in_array( $type_attr, $optimized_types, true ) ) {
417 continue;
418 }
419
420 $alternative_sources = $this->create_alternative_sources( $source_tag_html );
421 if ( is_array( $alternative_sources ) && $alternative_sources ) {
422 foreach ( $alternative_sources as $alt ) {
423 $modified_sources[] = $alt; // no array_merge in the loop
424 }
425 }
426 }
427
428 // handle inner image
429 foreach ( $this->get_element_by_tag( $this->raw_html, 'img' ) as $img_tag_html ) {
430 $alt_image_source = $this->create_alternative_sources( $img_tag_html );
431 $modified_sources = array_merge( $modified_sources, $alt_image_source );
432 }
433
434 $modified_source = implode( '', $modified_sources );
435
436 // Insert newly built <source> elements immediately before the first <img>
437 return preg_replace( '#(<img\b)#i', $modified_source . '$1', $this->raw_html, 1 );
438 }
439 }
440
441 class Tiny_Image_Source extends Tiny_Source_Base {
442
443 /**
444 * Generates a formatted image source array if the corresponding local file exists.
445 *
446 * Attempts to replace the file extension of the provided image path with the
447 * specified MIME type, resolves the local path of the resulting file, and returns
448 * the `srcset` and `type` if the file exists.
449 *
450 * @return string a <picture> element contain additional sources
451 */
452 public function create_picture_elements() {
453 $sources = $this->create_alternative_sources( $this->raw_html );
454 if ( empty( $sources ) ) {
455 return $this->raw_html;
456 }
457 $picture_element = array( '<picture>' );
458 $picture_element[] = implode( '', $sources );
459 $picture_element[] = $this->raw_html;
460 $picture_element[] = '</picture>';
461
462 return implode( '', $picture_element );
463 }
464 }
465