PluginProbe ʕ •ᴥ•ʔ
TinyPNG – JPEG, PNG & WebP image compression / trunk
TinyPNG – JPEG, PNG & WebP image compression vtrunk
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-source-base.php
tiny-compress-images / src Last commit date
compatibility 1 day ago config 1 day ago css 5 months ago data 3 years ago images 3 years ago js 1 day ago vendor 4 months ago views 1 day ago class-tiny-apache-rewrite.php 1 day ago class-tiny-bulk-optimization.php 1 day ago class-tiny-cli.php 1 day ago class-tiny-compress-client.php 1 day ago class-tiny-compress-fopen.php 1 day ago class-tiny-compress.php 1 day ago class-tiny-conversion.php 2 months ago class-tiny-diagnostics.php 5 months ago class-tiny-exception.php 5 months ago class-tiny-helpers.php 1 day ago class-tiny-image-size.php 1 day ago class-tiny-image.php 1 day ago class-tiny-logger.php 1 day ago class-tiny-migrate.php 1 day ago class-tiny-notices.php 1 day ago class-tiny-php.php 1 day ago class-tiny-picture.php 1 day ago class-tiny-plugin.php 1 day ago class-tiny-settings.php 1 day ago class-tiny-source-base.php 2 months ago class-tiny-source-image.php 5 months ago class-tiny-source-picture.php 5 months ago class-tiny-wp-base.php 1 day ago
class-tiny-source-base.php
312 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 abstract class Tiny_Source_Base {
23
24 public $raw_html;
25 protected $base_dir;
26 protected $allowed_domains;
27 protected $valid_mimetypes;
28
29 public function __construct( $html, $base_dir, $domains ) {
30 $this->raw_html = $html;
31 $this->base_dir = $base_dir;
32 $this->allowed_domains = $domains;
33 $this->valid_mimetypes = array( 'image/avif', 'image/webp' );
34 }
35
36 protected static function get_attribute_value( $element, $name ) {
37 // Match the exact attribute name (not part of data-media, mediaType, etc.)
38 // and capture a single- or double-quoted value.
39 $delim = '~';
40 $attr = preg_quote( $name, $delim );
41 $regex = $delim . '(?<![\w:-])' . $attr . '\s*=\s*(["\'])(.*?)\1' . $delim . 'is';
42
43 if ( preg_match( $regex, $element, $m ) ) {
44 return $m[2];
45 }
46 return null;
47 }
48
49 /**
50 * Extract elements by tag name from an HTML string (regex-based).
51 *
52 * @param string $html The HTML string to search in.
53 * @param string $tagname The tag name (e.g., 'div', 'source', 'img').
54 * @return array Array of matched elements as strings.
55 */
56 protected function get_element_by_tag( $html, $tagname ) {
57 $results = array();
58
59 // Self-closing / void tag (e.g. <source />, <img />, <br />)
60 if ( preg_match_all(
61 '~<' . preg_quote( $tagname, '~' ) . '\b(?:[^>"\']+|"[^"]*"|\'[^\']*\')*/?>~i',
62 $html,
63 $matches
64 ) ) {
65 $results = array_merge( $results, $matches[0] );
66 }
67
68 // Normal paired tags (e.g. <div>…</div>)
69 $regex_tag = preg_quote( $tagname, '~' );
70 if ( preg_match_all(
71 '~<' . $regex_tag .
72 '\b(?:[^>"\']+|"[^"]*"|\'[^\']*\')*>.*?</' .
73 $regex_tag .
74 '>~is',
75 $html,
76 $matches
77 ) ) {
78 $results = array_merge( $results, $matches[0] );
79 }
80
81 return $results;
82 }
83
84 protected function get_local_path( $url ) {
85 if ( strpos( $url, 'http' ) === 0 ) {
86 $matched_domain = null;
87
88 foreach ( $this->allowed_domains as $domain ) {
89 if ( strpos( $url, $domain ) === 0 ) {
90 $matched_domain = $domain;
91 break;
92 }
93 }
94
95 if ( null === $matched_domain ) {
96 return '';
97 }
98
99 $url = substr( $url, strlen( $matched_domain ) );
100 }
101 $url = $this->base_dir . $url;
102
103 return $url;
104 }
105
106 protected function get_formatted_source( $image_source_data, $mimetype ) {
107 $format_url = Tiny_Helpers::replace_file_extension( $mimetype, $image_source_data['path'] );
108 $local_path = $this->get_local_path( $format_url );
109 if ( empty( $local_path ) ) {
110 return null;
111 }
112
113 $exists_local = file_exists( $local_path );
114 if ( $exists_local ) {
115 return array(
116 'src' => $format_url,
117 'size' => $image_source_data['size'],
118 'type' => $mimetype,
119 );
120 }
121 return null;
122 }
123
124 /**
125 * Retrieves the sources from the <img> or <source> element
126 *
127 * @return array{path: string, size: string}[] The image sources
128 */
129 protected function get_image_srcsets( $html ) {
130 $result = array();
131 $srcset = $this::get_attribute_value( $html, 'srcset' );
132
133 if ( $srcset ) {
134 // Split the srcset to get individual entries
135 $srcset_entries = explode( ',', $srcset );
136
137 foreach ( $srcset_entries as $entry ) {
138 // Trim whitespace
139 $entry = trim( $entry );
140
141 // Split by whitespace to separate path and size/density descriptor
142 $parts = preg_split( '/\s+/', $entry, 2 );
143
144 if ( count( $parts ) === 2 ) {
145 // We have both path and size
146 $result[] = array(
147 'path' => $parts[0],
148 'size' => $parts[1],
149 );
150 } elseif ( count( $parts ) === 1 ) {
151 // We only have a path, will be interpreted as pixel
152 // density 1x (unusual in srcset)
153 $result[] = array(
154 'path' => $parts[0],
155 'size' => '',
156 );
157 }
158 }
159 }
160 return $result;
161 }
162
163 /**
164 * Retrieves the sources from the <img> or <source> element
165 *
166 * @return array{path: string, size: string}[] The image sources
167 */
168 private function get_image_src( $html ) {
169 $source = $this::get_attribute_value( $html, 'src' );
170 if ( ! empty( $source ) ) {
171 // No srcset, but we have a src attribute
172 return array(
173 'path' => $source,
174 'size' => '',
175 );
176 }
177 return array();
178 }
179
180
181 /**
182 * Creates one or more <source> elements if alternative formats
183 * are available.
184 *
185 * @param string $original_source_html, either <source> or <img>
186 * @return array{string} array of <source> html
187 */
188 protected function create_alternative_sources( $original_source_html ) {
189 $srcsets = $this->get_image_srcsets( $original_source_html );
190 if ( empty( $srcsets ) ) {
191 // no srcset, try src attribute
192 $src = $this->get_image_src( $original_source_html );
193 if ( ! empty( $src ) ) {
194 $srcsets[] = $src;
195 }
196 }
197
198 if ( empty( $srcsets ) ) {
199 return array();
200 }
201
202 $is_source_tag = (bool) preg_match( '#<source\b#i', $original_source_html );
203
204 $sources = array();
205 $width_descriptor = $this->get_largest_width_descriptor( $srcsets );
206
207 foreach ( $this->valid_mimetypes as $mimetype ) {
208 $srcset_parts = array();
209
210 foreach ( $srcsets as $srcset ) {
211 $alt_source = $this->get_formatted_source( $srcset, $mimetype );
212 if ( $alt_source ) {
213 $srcset_parts[] = trim( $alt_source['src'] . ' ' . $alt_source['size'] );
214 }
215 }
216
217 if (
218 $width_descriptor &&
219 ! self::srcset_contains_width_descriptor(
220 $srcset_parts,
221 $width_descriptor
222 )
223 ) {
224 continue;
225 }
226
227 if ( empty( $srcset_parts ) ) {
228 continue;
229 }
230
231 $source_attr_parts = array();
232
233 $srcset_attr = implode( ', ', $srcset_parts );
234 $source_attr_parts['srcset'] = $srcset_attr;
235
236 if ( $is_source_tag ) {
237 foreach ( array( 'sizes', 'media', 'width', 'height' ) as $attr ) {
238 $attr_value = $this->get_attribute_value( $original_source_html, $attr );
239 if ( $attr_value ) {
240 $source_attr_parts[ $attr ] = $attr_value;
241 }
242 }
243 } else {
244 $sizes_value = $this->get_attribute_value( $original_source_html, 'sizes' );
245 if ( $sizes_value ) {
246 $source_attr_parts['sizes'] = $sizes_value;
247 }
248 }
249
250 $source_attr_parts['type'] = $mimetype;
251 $source_parts = array( '<source' );
252 foreach ( $source_attr_parts as $source_attr_name => $source_attr_val ) {
253 $source_parts[] = $source_attr_name . '="' . esc_attr( $source_attr_val ) . '"';
254 }
255 $source_parts[] = '/>';
256 $sources[] = implode( ' ', $source_parts );
257 } // End foreach().
258
259 return $sources;
260 }
261
262 /**
263 * Returns the largest numeric width descriptor
264 * (e.g. 2000 from "2000w") found in the srcset data.
265 *
266 * @param array<array{path: string, size: string}> $srcsets
267 * @return int
268 */
269 public static function get_largest_width_descriptor( $srcsets ) {
270 $largest = 0;
271
272 foreach ( $srcsets as $srcset ) {
273 if ( empty( $srcset['size'] ) ) {
274 continue;
275 }
276
277 if ( preg_match( '/(\d+)w/', $srcset['size'], $matches ) ) {
278 $width = (int) $matches[1];
279 if ( $width > $largest ) {
280 $largest = $width;
281 }
282 }
283 }
284
285 return $largest;
286 }
287
288 /**
289 * Determines whether a srcset list contains the provided width descriptor.
290 *
291 * @param string[] $srcset_parts
292 * @param int $width_descriptor
293 * @return bool true if width is in srcset
294 */
295 public static function srcset_contains_width_descriptor( $srcset_parts, $width_descriptor ) {
296 if ( empty( $srcset_parts ) || $width_descriptor <= 0 ) {
297 return false;
298 }
299
300 $suffix = ' ' . $width_descriptor . 'w';
301 $suffix_length = strlen( $suffix );
302
303 foreach ( $srcset_parts as $srcset_part ) {
304 if ( substr( $srcset_part, -$suffix_length ) === $suffix ) {
305 return true;
306 }
307 }
308
309 return false;
310 }
311 }
312