PluginProbe ʕ •ᴥ•ʔ
TinyPNG – JPEG, PNG & WebP image compression / 3.6.4
TinyPNG – JPEG, PNG & WebP image compression v3.6.4
3.8.0 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-image.php
tiny-compress-images / src Last commit date
compatibility 1 year ago config 4 years ago css 10 months ago data 4 years ago images 4 years ago js 9 months 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 9 months ago class-tiny-compress-fopen.php 9 months ago class-tiny-compress.php 9 months ago class-tiny-exception.php 4 years ago class-tiny-helpers.php 1 year ago class-tiny-image-size.php 10 months ago class-tiny-image.php 9 months ago class-tiny-notices.php 11 months ago class-tiny-php.php 4 years ago class-tiny-picture.php 9 months ago class-tiny-plugin.php 9 months ago class-tiny-settings.php 9 months ago class-tiny-wp-base.php 11 months ago
class-tiny-image.php
561 lines
1 <?php
2 /*
3 * Tiny Compress Images - WordPress plugin.
4 * Copyright (C) 2015-2018 Tinify B.V.
5 *
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the Free
8 * Software Foundation; either version 2 of the License, or (at your option)
9 * any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 * more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc., 51
18 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21 class Tiny_Image {
22 const ORIGINAL = 0;
23
24 /** @var Tiny_Settings */
25 private $settings;
26 private $id;
27 private $name;
28 private $wp_metadata;
29 private $sizes = array();
30 private $statistics = array();
31
32 public function __construct(
33 $settings,
34 $id,
35 $wp_metadata = null,
36 $tiny_metadata = null,
37 $active_sizes = null,
38 $active_tinify_sizes = null
39 ) {
40 $this->settings = $settings;
41 $this->id = $id;
42 $this->wp_metadata = $wp_metadata;
43 $this->parse_wp_metadata();
44 $this->parse_tiny_metadata( $tiny_metadata );
45 $this->detect_duplicates( $active_sizes, $active_tinify_sizes );
46 }
47
48 private function parse_wp_metadata() {
49 if ( ! is_array( $this->wp_metadata ) ) {
50 $this->wp_metadata = wp_get_attachment_metadata( $this->id );
51 }
52
53 if ( ! is_array( $this->wp_metadata ) || ! isset( $this->wp_metadata['file'] ) ) {
54 /* No file metadata found, this might be another plugin messing with
55 metadata. Simply ignore this! */
56 return;
57 }
58
59 $upload_dir = wp_upload_dir();
60 $path_prefix = $upload_dir['basedir'] . '/';
61 $path_info = pathinfo( $this->wp_metadata['file'] );
62 if ( isset( $path_info['dirname'] ) ) {
63 $path_prefix .= $path_info['dirname'] . '/';
64 }
65
66 /* Do not use pathinfo for getting the filename.
67 It doesn't work when the filename starts with a special character. */
68 $path_parts = explode( '/', $this->wp_metadata['file'] );
69 $this->name = end( $path_parts );
70 $filename = $path_prefix . $this->name;
71 $this->sizes[ self::ORIGINAL ] = new Tiny_Image_Size( $filename );
72
73 // Ensure 'sizes' exists and is an array to prevent PHP Warnings
74 $sizes = isset( $this->wp_metadata['sizes'] ) && is_array( $this->wp_metadata['sizes'] )
75 ? $this->wp_metadata['sizes']
76 : array();
77
78 $sanitized_sizes = array();
79 foreach ( $sizes as $size_name => $size_info ) {
80 // size is valid when its an array and has a file
81 if ( is_array( $size_info ) && isset( $size_info['file'] ) ) {
82 // Add to sanitized metadata
83 $sanitized_sizes[ $size_name ] = $size_info;
84 $this->sizes[ $size_name ] = new Tiny_Image_Size(
85 $path_prefix . $size_info['file']
86 );
87 }
88 }
89
90 // Update the metadata with only the valid sizes found
91 $this->wp_metadata['sizes'] = $sanitized_sizes;
92 }
93
94 private function detect_duplicates( $active_sizes, $active_tinify_sizes ) {
95 $filenames = array();
96
97 if ( is_array( $this->wp_metadata )
98 && isset( $this->wp_metadata['file'] )
99 && isset( $this->wp_metadata['sizes'] )
100 && is_array( $this->wp_metadata['sizes'] ) ) {
101
102 if ( null == $active_sizes ) {
103 $active_sizes = $this->settings->get_sizes();
104 }
105 if ( null == $active_tinify_sizes ) {
106 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
107 }
108
109 foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
110 if ( $this->sizes[ $size_name ]->has_been_compressed()
111 && array_key_exists( $size_name, $active_sizes ) ) {
112 $filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
113 }
114 }
115 foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
116 if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
117 $filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
118 }
119 }
120 foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
121 if ( array_key_exists( $size_name, $active_sizes ) ) {
122 $filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
123 }
124 }
125 foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
126 $filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
127 }
128 }
129 }
130
131 private function duplicate_check( $filenames, $file, $size_name ) {
132 if ( isset( $filenames[ $file ] ) ) {
133 if ( $filenames[ $file ] != $size_name ) {
134 $this->sizes[ $size_name ]->mark_duplicate( $filenames[ $file ] );
135 }
136 } else {
137 $filenames[ $file ] = $size_name;
138 }
139 return $filenames;
140 }
141
142 private function parse_tiny_metadata( $tiny_metadata = null ) {
143 if ( is_null( $tiny_metadata ) ) {
144 $tiny_metadata = get_post_meta( $this->id, Tiny_Config::META_KEY, true );
145 }
146 if ( $tiny_metadata ) {
147 foreach ( $tiny_metadata as $size => $meta ) {
148 if ( ! isset( $this->sizes[ $size ] ) ) {
149 if ( self::is_retina( $size ) && Tiny_Settings::wr2x_active() ) {
150 $size_name = rtrim( $size, '_wr2x' );
151 if ( 'original' === $size_name ) {
152 $size_name = '0';
153 }
154 $retina_path = wr2x_get_retina(
155 $this->sizes[ $size_name ]->filename
156 );
157 $this->sizes[ $size ] = new Tiny_Image_Size( $retina_path );
158 } else {
159 $this->sizes[ $size ] = new Tiny_Image_Size();
160 }
161 }
162 $this->sizes[ $size ]->meta = $meta;
163 }
164 }
165 }
166
167 public function get_id() {
168 return $this->id;
169 }
170
171 public function get_name() {
172 return $this->name;
173 }
174
175 public function get_wp_metadata() {
176 return $this->wp_metadata;
177 }
178
179 public function file_type_allowed() {
180 return in_array( $this->get_mime_type(), array( 'image/jpeg', 'image/png', 'image/webp' ) );
181 }
182
183 public function get_mime_type() {
184 return get_post_mime_type( $this->id );
185 }
186
187 public function compress() {
188 if ( $this->settings->get_compressor() === null || ! $this->file_type_allowed() ) {
189 return;
190 }
191
192 $success = 0;
193 $failed = 0;
194
195 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
196
197 if ( $this->settings->get_conversion_enabled() ) {
198 $uncompressed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );
199 $unconverted_sizes = $this->filter_image_sizes( 'unconverted', $active_tinify_sizes );
200
201 $unprocessed_sizes = $uncompressed_sizes + $unconverted_sizes;
202 } else {
203 $unprocessed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );
204 }
205
206 $compressor = $this->settings->get_compressor();
207 $convert_to = $this->convert_to();
208
209 foreach ( $unprocessed_sizes as $size_name => $size ) {
210 if ( ! $size->is_duplicate() ) {
211 $size->add_tiny_meta_start();
212 $this->update_tiny_post_meta();
213 $resize = $this->settings->get_resize_options( $size_name );
214 $preserve = $this->settings->get_preserve_options( $size_name );
215 try {
216 $response = $compressor->compress_file(
217 $size->filename,
218 $resize,
219 $preserve,
220 $convert_to
221 );
222
223 // ensure that all conversion are in the same format as the first one
224 $convert_to = isset( $response['convert'] ) ?
225 array( $response['convert']['type'] ) :
226 $convert_to;
227
228 $size->add_tiny_meta( $response );
229 $success++;
230 } catch ( Tiny_Exception $e ) {
231 $size->add_tiny_meta_error( $e );
232 $failed++;
233 }
234 $this->add_wp_metadata( $size_name, $size );
235 $this->update_tiny_post_meta();
236 }
237 }
238
239 /*
240 Other plugins can hook into this action to execute custom logic
241 after the image sizes have been compressed, ie. cache flushing.
242 */
243 do_action( 'tiny_image_after_compression', $this->id, $success );
244
245 return array(
246 'success' => $success,
247 'failed' => $failed,
248 );
249 }
250
251 public function delete_converted_image() {
252 $sizes = $this->get_image_sizes();
253 foreach ( $sizes as $size ) {
254 $size->delete_converted_image_size();
255 }
256 }
257
258 public function compress_retina( $size_name, $path ) {
259 if ( $this->settings->get_compressor() === null || ! $this->file_type_allowed() ) {
260 return;
261 }
262
263 if ( ! isset( $this->sizes[ $size_name ] ) ) {
264 $this->sizes[ $size_name ] = new Tiny_Image_Size( $path );
265 }
266
267 $size = $this->sizes[ $size_name ];
268
269 $compressor = $this->settings->get_compressor();
270 $convert_to = $this->convert_to();
271
272 if ( ! $size->has_been_compressed() ) {
273 $size->add_tiny_meta_start();
274 $this->update_tiny_post_meta();
275 $preserve = $this->settings->get_preserve_options( $size_name );
276
277 try {
278 $response = $compressor->compress_file( $path, false, $preserve, $convert_to );
279 $size->add_tiny_meta( $response );
280 } catch ( Tiny_Exception $e ) {
281 $size->add_tiny_meta_error( $e );
282 }
283 $this->update_tiny_post_meta();
284 }
285 }
286
287 public function remove_retina_metadata() {
288 // Remove metadata from all sizes, as this callback only fires when all
289 // retina sizes are deleted.
290 foreach ( $this->sizes as $size_name => $size ) {
291 if ( self::is_retina( $size_name ) ) {
292 unset( $this->sizes[ $size_name ] );
293 }
294 }
295 $this->update_tiny_post_meta();
296 }
297
298 public function add_wp_metadata( $size_name, $size ) {
299 if ( self::is_original( $size_name ) ) {
300 if ( isset( $size->meta['output'] ) ) {
301 $output = $size->meta['output'];
302 if ( isset( $output['width'] ) && isset( $output['height'] ) ) {
303 $this->wp_metadata['width'] = $output['width'];
304 $this->wp_metadata['height'] = $output['height'];
305 $this->wp_metadata['filesize'] = $output['size'];
306 }
307 }
308 }
309 }
310
311 public function update_tiny_post_meta() {
312 $tiny_metadata = array();
313 foreach ( $this->sizes as $size_name => $size ) {
314 $tiny_metadata[ $size_name ] = $size->meta;
315 }
316 update_post_meta( $this->id, Tiny_Config::META_KEY, $tiny_metadata );
317 /*
318 This action is being used by WPML:
319 https://gist.github.com/srdjan-jcc/5c47685cda4da471dff5757ba3ce5ab1
320 */
321 do_action( 'updated_tiny_postmeta', $this->id, Tiny_Config::META_KEY, $tiny_metadata );
322 }
323
324 public function get_image_sizes() {
325 $original = isset( $this->sizes[ self::ORIGINAL ] )
326 ? array(
327 self::ORIGINAL => $this->sizes[ self::ORIGINAL ],
328 )
329 : array();
330 $compressed = array();
331 $uncompressed = array();
332 foreach ( $this->sizes as $size_name => $size ) {
333 if ( self::is_original( $size_name ) ) {
334 continue;
335 }
336
337 if ( $size->has_been_compressed() ) {
338 $compressed[ $size_name ] = $size;
339 } else {
340 $uncompressed[ $size_name ] = $size;
341 }
342 }
343 ksort( $compressed );
344 ksort( $uncompressed );
345 return $original + $compressed + $uncompressed;
346 }
347
348 public function get_image_size( $size = self::ORIGINAL, $create = false ) {
349 if ( isset( $this->sizes[ $size ] ) ) {
350 return $this->sizes[ $size ];
351 } elseif ( $create ) {
352 return new Tiny_Image_Size();
353 } else {
354 return null;
355 }
356 }
357
358 public function filter_image_sizes( $method, $filter_sizes = null ) {
359 $selection = array();
360 if ( is_null( $filter_sizes ) ) {
361 $filter_sizes = array_keys( $this->sizes );
362 }
363 foreach ( $filter_sizes as $size_name ) {
364 if ( ! isset( $this->sizes[ $size_name ] ) ) {
365 continue;
366 }
367
368 $tiny_image_size = $this->sizes[ $size_name ];
369 if ( $tiny_image_size->$method() ) {
370 $selection[ $size_name ] = $tiny_image_size;
371 }
372 }
373 return $selection;
374 }
375
376 public function get_count( $methods, $count_sizes = null ) {
377 $stats = array_fill_keys( $methods, 0 );
378 if ( is_null( $count_sizes ) ) {
379 $count_sizes = array_keys( $this->sizes );
380 }
381 foreach ( $count_sizes as $size ) {
382 if ( ! isset( $this->sizes[ $size ] ) ) {
383 continue;
384 }
385
386 foreach ( $methods as $method ) {
387 if ( $this->sizes[ $size ]->$method() ) {
388 $stats[ $method ]++;
389 }
390 }
391 }
392 return $stats;
393 }
394
395 public function get_latest_error() {
396 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
397 $error_message = null;
398 $last_timestamp = null;
399 foreach ( $this->sizes as $size_name => $size ) {
400 if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
401 if ( isset( $size->meta['error'] ) && isset( $size->meta['message'] ) ) {
402 if ( null === $last_timestamp || $last_timestamp < $size->meta['timestamp'] ) {
403 $last_timestamp = $size->meta['timestamp'];
404 $error_message = Tiny_Helpers::truncate_text( $size->meta['message'], 140 );
405 }
406 }
407 }
408 }
409 return $error_message;
410 }
411
412 public function get_savings( $stats ) {
413 $before = $stats['initial_total_size'];
414 $after = $stats['compressed_total_size'];
415 if ( 0 === $before ) {
416 $savings = 0;
417 } else {
418 $savings = ($before - $after) / $before * 100;
419 }
420 return '' . number_format( $savings, 1 );
421 }
422
423 public function get_statistics( $active_sizes, $active_tinify_sizes ) {
424 if ( $this->statistics ) {
425 error_log( 'Strangely the image statistics are asked for again.' );
426 return $this->statistics;
427 }
428
429 $this->statistics['initial_total_size'] = 0;
430 $this->statistics['compressed_total_size'] = 0;
431 $this->statistics['image_sizes_compressed'] = 0;
432 $this->statistics['available_uncompressed_sizes'] = 0;
433 $this->statistics['image_sizes_converted'] = 0;
434 $this->statistics['available_unconverted_sizes'] = 0;
435
436 foreach ( $this->sizes as $size_name => $size ) {
437 // skip duplicates or inactive sizes
438 if ( $size->is_duplicate() || ! isset( $active_sizes[ $size_name ] ) ) {
439 continue;
440 }
441
442 $file_size = $size->filesize();
443 $is_active_size = in_array( $size_name, $active_tinify_sizes, true );
444
445 if ( isset( $size->meta['input'] ) ) {
446 $input_size = (int) $size->meta['input']['size'];
447 $this->statistics['initial_total_size'] += $input_size;
448
449 if ( isset( $size->meta['output'] ) ) {
450 $output_size = (int) $size->meta['output']['size'];
451
452 if ( $size->modified() ) {
453 $this->statistics['compressed_total_size'] += $file_size;
454 if ( $is_active_size ) {
455 $this->statistics['available_uncompressed_sizes']++;
456 }
457 } else {
458 $this->statistics['compressed_total_size'] += $output_size;
459 $this->statistics['image_sizes_compressed']++;
460 }
461 } else {
462 $this->statistics['compressed_total_size'] += $input_size;
463 }
464 } elseif ( $size->exists() ) {
465 $this->statistics['initial_total_size'] += $file_size;
466 $this->statistics['compressed_total_size'] += $file_size;
467 if ( $is_active_size ) {
468 $this->statistics['available_uncompressed_sizes']++;
469 }
470 }
471
472 if ( $is_active_size ) {
473 if ( $size->has_been_converted() ) {
474 $this->statistics['image_sizes_converted']++;
475 } else {
476 $this->statistics['available_unconverted_sizes']++;
477 }
478 }
479 }// End foreach().
480
481 return $this->statistics;
482 }
483
484
485 public static function is_original( $size ) {
486 return self::ORIGINAL === $size;
487 }
488
489 public static function is_retina( $size ) {
490 return strrpos( $size, 'wr2x' ) === strlen( $size ) - strlen( 'wr2x' );
491 }
492
493 public function can_be_converted() {
494 return $this->settings->get_conversion_enabled() && $this->file_type_allowed();
495 }
496
497 /**
498 * Get the targeted conversion.
499 * If original is already converted, then we use the originals' mimetype.
500 * If nothing is converted yet, we use the settings conversion settings.
501 *
502 * @since 3.6.4
503 *
504 * @return array{string} mimetypes to which the image should be converted to
505 */
506 private function convert_to() {
507 $convert_settings = $this->settings->get_conversion_options();
508 if ( ! $convert_settings['convert'] ) {
509 // conversion is off so return no mimetypes to convert to
510 return array();
511 }
512
513 if ( isset( $this->sizes[ self::ORIGINAL ] ) ) {
514 // original is not in sizes so mimetypes are open
515 return $convert_settings['convert_to'];
516 }
517
518 $original_img_size = $this->sizes[ self::ORIGINAL ];
519 if ( $original_img_size->converted() ) {
520 // original has been convert so use that mimetype to convert to
521 return array( $original_img_size->meta['convert']['type'] );
522 }
523
524 return $convert_settings['convert_to'];
525
526 }
527
528 /**
529 * Marks the image as compressed without actually compressing it.
530 *
531 * This method parses existing metadata and delegates to each image size to mark
532 * itself as compressed. It considers conversion settings when marking the sizes.
533 * This is useful for images that are already optimized or when you want to skip
534 * compression while still marking them as processed in the system.
535 *
536 * @since 3.0.0
537 */
538 public function mark_as_compressed() {
539 $this->parse_tiny_metadata();
540
541 $conversion_enabled = $this->settings->get_conversion_enabled();
542
543 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
544
545 if ( $this->settings->get_conversion_enabled() ) {
546 $uncompressed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );
547 $unconverted_sizes = $this->filter_image_sizes( 'unconverted', $active_tinify_sizes );
548
549 $unprocessed_sizes = $uncompressed_sizes + $unconverted_sizes;
550 } else {
551 $unprocessed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );
552 }
553
554 foreach ( $unprocessed_sizes as $size ) {
555 $size->mark_as_compressed( $conversion_enabled );
556 }
557
558 $this->update_tiny_post_meta();
559 }
560 }
561