PluginProbe ʕ •ᴥ•ʔ
TinyPNG – JPEG, PNG & WebP image compression / 3.6.13
TinyPNG – JPEG, PNG & WebP image compression v3.6.13
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 5 months ago config 5 months ago css 5 months ago data 3 years ago images 3 years ago js 5 months ago vendor 4 months ago views 2 months ago class-tiny-apache-rewrite.php 2 months ago class-tiny-bulk-optimization.php 5 months ago class-tiny-cli.php 5 months ago class-tiny-compress-client.php 5 months ago class-tiny-compress-fopen.php 5 months ago class-tiny-compress.php 5 months 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 5 months ago class-tiny-image-size.php 5 months ago class-tiny-image.php 5 months ago class-tiny-logger.php 2 months ago class-tiny-notices.php 2 months ago class-tiny-php.php 5 months ago class-tiny-picture.php 4 months ago class-tiny-plugin.php 2 months ago class-tiny-settings.php 2 months 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 5 months ago
class-tiny-image.php
609 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(
181 $this->get_mime_type(),
182 array( 'image/jpeg', 'image/png', 'image/webp' ),
183 true
184 );
185 }
186
187 public function get_mime_type() {
188 return get_post_mime_type( $this->id );
189 }
190
191 public function compress() {
192 Tiny_Logger::debug(
193 'compress',
194 array(
195 'image_id' => $this->id,
196 'name' => $this->name,
197 )
198 );
199 if ( $this->settings->get_compressor() === null || ! $this->file_type_allowed() ) {
200 return;
201 }
202
203 $success = 0;
204 $failed = 0;
205
206 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
207
208 if ( $this->settings->get_conversion_enabled() ) {
209 $uncompressed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );
210 $unconverted_sizes = $this->filter_image_sizes( 'unconverted', $active_tinify_sizes );
211
212 $unprocessed_sizes = $uncompressed_sizes + $unconverted_sizes;
213 } else {
214 $unprocessed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );
215 }
216
217 $compressor = $this->settings->get_compressor();
218 $convert_to = $this->convert_to();
219
220 foreach ( $unprocessed_sizes as $size_name => $size ) {
221 if ( ! $size->is_duplicate() ) {
222 $size->add_tiny_meta_start();
223 $this->update_tiny_post_meta();
224 $resize = $this->settings->get_resize_options( $size_name );
225 $preserve = $this->settings->get_preserve_options( $size_name );
226 Tiny_Logger::debug(
227 'compress size',
228 array(
229 'image_id' => $this->id,
230 'size' => $size_name,
231 'resize' => $resize,
232 'preserve' => $preserve,
233 'convert' => $convert_to,
234 'modified' => $size->modified(),
235 'filename' => $size->filename,
236 'is_duplicate' => $size->is_duplicate(),
237 'exists' => $size->exists(),
238 'has_been_compressed' => $size->has_been_compressed(),
239 'filesize' => $size->filesize(),
240 'mimetype' => $size->mimetype(),
241 )
242 );
243 try {
244 $response = $compressor->compress_file(
245 $size->filename,
246 $resize,
247 $preserve,
248 $convert_to
249 );
250
251 // ensure that all conversion are in the same format as the first one
252 $convert_to = isset( $response['convert'] ) ?
253 array( $response['convert']['type'] ) :
254 $convert_to;
255
256 $size->add_tiny_meta( $response );
257 ++$success;
258 Tiny_Logger::debug(
259 'compress success',
260 array(
261 'size' => $size_name,
262 'image_id' => $this->id,
263 )
264 );
265 } catch ( Tiny_Exception $e ) {
266 $size->add_tiny_meta_error( $e );
267 ++$failed;
268 Tiny_Logger::error(
269 'compress failed',
270 array(
271 'error' => $e->get_message(),
272 'size' => $size_name,
273 'image_id' => $this->id,
274 )
275 );
276 }
277 $this->add_wp_metadata( $size_name, $size );
278 $this->update_tiny_post_meta();
279 }// End if().
280 }// End foreach().
281
282 /*
283 Other plugins can hook into this action to execute custom logic
284 after the image sizes have been compressed, ie. cache flushing.
285 */
286 do_action( 'tiny_image_after_compression', $this->id, $success );
287
288 return array(
289 'success' => $success,
290 'failed' => $failed,
291 );
292 }
293
294 public function delete_converted_image() {
295 $sizes = $this->get_image_sizes();
296 foreach ( $sizes as $size ) {
297 $size->delete_converted_image_size();
298 }
299 }
300
301 public function compress_retina( $size_name, $path ) {
302 if ( $this->settings->get_compressor() === null || ! $this->file_type_allowed() ) {
303 return;
304 }
305
306 if ( ! isset( $this->sizes[ $size_name ] ) ) {
307 $this->sizes[ $size_name ] = new Tiny_Image_Size( $path );
308 }
309
310 $size = $this->sizes[ $size_name ];
311
312 $compressor = $this->settings->get_compressor();
313 $convert_to = $this->convert_to();
314
315 if ( ! $size->has_been_compressed() ) {
316 $size->add_tiny_meta_start();
317 $this->update_tiny_post_meta();
318 $preserve = $this->settings->get_preserve_options( $size_name );
319
320 try {
321 $response = $compressor->compress_file( $path, false, $preserve, $convert_to );
322 $size->add_tiny_meta( $response );
323 } catch ( Tiny_Exception $e ) {
324 $size->add_tiny_meta_error( $e );
325 }
326 $this->update_tiny_post_meta();
327 }
328 }
329
330 public function remove_retina_metadata() {
331 // Remove metadata from all sizes, as this callback only fires when all
332 // retina sizes are deleted.
333 foreach ( $this->sizes as $size_name => $size ) {
334 if ( self::is_retina( $size_name ) ) {
335 unset( $this->sizes[ $size_name ] );
336 }
337 }
338 $this->update_tiny_post_meta();
339 }
340
341 public function add_wp_metadata( $size_name, $size ) {
342 if ( self::is_original( $size_name ) ) {
343 if ( isset( $size->meta['output'] ) ) {
344 $output = $size->meta['output'];
345 if ( isset( $output['width'] ) && isset( $output['height'] ) ) {
346 $this->wp_metadata['width'] = $output['width'];
347 $this->wp_metadata['height'] = $output['height'];
348 $this->wp_metadata['filesize'] = $output['size'];
349 }
350 }
351 }
352 }
353
354 public function update_tiny_post_meta() {
355 $tiny_metadata = array();
356 foreach ( $this->sizes as $size_name => $size ) {
357 $tiny_metadata[ $size_name ] = $size->meta;
358 }
359 update_post_meta( $this->id, Tiny_Config::META_KEY, $tiny_metadata );
360 /*
361 This action is being used by WPML:
362 https://gist.github.com/srdjan-jcc/5c47685cda4da471dff5757ba3ce5ab1
363 */
364 do_action( 'updated_tiny_postmeta', $this->id, Tiny_Config::META_KEY, $tiny_metadata );
365 }
366
367 public function get_image_sizes() {
368 $original = isset( $this->sizes[ self::ORIGINAL ] )
369 ? array(
370 self::ORIGINAL => $this->sizes[ self::ORIGINAL ],
371 )
372 : array();
373 $compressed = array();
374 $uncompressed = array();
375 foreach ( $this->sizes as $size_name => $size ) {
376 if ( self::is_original( $size_name ) ) {
377 continue;
378 }
379
380 if ( $size->has_been_compressed() ) {
381 $compressed[ $size_name ] = $size;
382 } else {
383 $uncompressed[ $size_name ] = $size;
384 }
385 }
386 ksort( $compressed );
387 ksort( $uncompressed );
388 return $original + $compressed + $uncompressed;
389 }
390
391 public function get_image_size( $size = self::ORIGINAL, $create = false ) {
392 if ( isset( $this->sizes[ $size ] ) ) {
393 return $this->sizes[ $size ];
394 } elseif ( $create ) {
395 return new Tiny_Image_Size();
396 } else {
397 return null;
398 }
399 }
400
401 public function filter_image_sizes( $method, $filter_sizes = null ) {
402 $selection = array();
403 if ( is_null( $filter_sizes ) ) {
404 $filter_sizes = array_keys( $this->sizes );
405 }
406 foreach ( $filter_sizes as $size_name ) {
407 if ( ! isset( $this->sizes[ $size_name ] ) ) {
408 continue;
409 }
410
411 $tiny_image_size = $this->sizes[ $size_name ];
412 if ( $tiny_image_size->$method() ) {
413 $selection[ $size_name ] = $tiny_image_size;
414 }
415 }
416 return $selection;
417 }
418
419 public function get_count( $methods, $count_sizes = null ) {
420 $stats = array_fill_keys( $methods, 0 );
421 if ( is_null( $count_sizes ) ) {
422 $count_sizes = array_keys( $this->sizes );
423 }
424 foreach ( $count_sizes as $size ) {
425 if ( ! isset( $this->sizes[ $size ] ) ) {
426 continue;
427 }
428
429 foreach ( $methods as $method ) {
430 if ( $this->sizes[ $size ]->$method() ) {
431 ++$stats[ $method ];
432 }
433 }
434 }
435 return $stats;
436 }
437
438 public function get_latest_error() {
439 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
440 $error_message = null;
441 $last_timestamp = null;
442 foreach ( $this->sizes as $size_name => $size ) {
443 if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
444 if ( isset( $size->meta['error'] ) && isset( $size->meta['message'] ) ) {
445 if (
446 null === $last_timestamp ||
447 $last_timestamp < $size->meta['timestamp']
448 ) {
449 $last_timestamp = $size->meta['timestamp'];
450 $error_message = Tiny_Helpers::truncate_text(
451 $size->meta['message'],
452 140
453 );
454 }
455 }
456 }
457 }
458 return $error_message;
459 }
460
461 public function get_savings( $stats ) {
462 $before = $stats['initial_total_size'];
463 $after = $stats['compressed_total_size'];
464 if ( 0 === $before ) {
465 $savings = 0;
466 } else {
467 $savings = ( $before - $after ) / $before * 100;
468 }
469 return '' . number_format( $savings, 1 );
470 }
471
472 public function get_statistics( $active_sizes, $active_tinify_sizes ) {
473 if ( $this->statistics ) {
474 error_log( 'Strangely the image statistics are asked for again.' );
475 return $this->statistics;
476 }
477
478 $this->statistics['initial_total_size'] = 0;
479 $this->statistics['compressed_total_size'] = 0;
480 $this->statistics['image_sizes_compressed'] = 0;
481 $this->statistics['available_uncompressed_sizes'] = 0;
482 $this->statistics['image_sizes_converted'] = 0;
483 $this->statistics['available_unconverted_sizes'] = 0;
484
485 foreach ( $this->sizes as $size_name => $size ) {
486 // skip duplicates or inactive sizes
487 if ( $size->is_duplicate() || ! isset( $active_sizes[ $size_name ] ) ) {
488 continue;
489 }
490
491 $file_size = $size->filesize();
492 $is_active_size = in_array( $size_name, $active_tinify_sizes, true );
493
494 if ( isset( $size->meta['input'] ) ) {
495 $input_size = (int) $size->meta['input']['size'];
496 $this->statistics['initial_total_size'] += $input_size;
497
498 if ( isset( $size->meta['output'] ) ) {
499 $output_size = (int) $size->meta['output']['size'];
500
501 if ( $size->modified() ) {
502 $this->statistics['compressed_total_size'] += $file_size;
503 if ( $is_active_size ) {
504 ++$this->statistics['available_uncompressed_sizes'];
505 }
506 } else {
507 $this->statistics['compressed_total_size'] += $output_size;
508 ++$this->statistics['image_sizes_compressed'];
509 }
510 } else {
511 $this->statistics['compressed_total_size'] += $input_size;
512 }
513 } elseif ( $size->exists() ) {
514 $this->statistics['initial_total_size'] += $file_size;
515 $this->statistics['compressed_total_size'] += $file_size;
516 if ( $is_active_size ) {
517 ++$this->statistics['available_uncompressed_sizes'];
518 }
519 }
520
521 if ( $is_active_size ) {
522 if ( $size->has_been_converted() ) {
523 ++$this->statistics['image_sizes_converted'];
524 } else {
525 ++$this->statistics['available_unconverted_sizes'];
526 }
527 }
528 }// End foreach().
529
530 return $this->statistics;
531 }
532
533
534 public static function is_original( $size ) {
535 return self::ORIGINAL === $size;
536 }
537
538 public static function is_retina( $size ) {
539 return strrpos( $size, 'wr2x' ) === strlen( $size ) - strlen( 'wr2x' );
540 }
541
542 public function can_be_converted() {
543 return $this->settings->get_conversion_enabled() && $this->file_type_allowed();
544 }
545
546 /**
547 * Get the targeted conversion.
548 * If original is already converted, then we use the originals' mimetype.
549 * If nothing is converted yet, we use the settings conversion settings.
550 *
551 * @since 3.6.4
552 *
553 * @return array{string} mimetypes to which the image should be converted to
554 */
555 private function convert_to() {
556 $convert_settings = $this->settings->get_conversion_options();
557 if ( ! $convert_settings['convert'] ) {
558 // conversion is off so return no mimetypes to convert to
559 return array();
560 }
561
562 if ( isset( $this->sizes[ self::ORIGINAL ] ) ) {
563 // original is not in sizes so mimetypes are open
564 return $convert_settings['convert_to'];
565 }
566
567 $original_img_size = $this->sizes[ self::ORIGINAL ];
568 if ( $original_img_size->converted() ) {
569 // original has been convert so use that mimetype to convert to
570 return array( $original_img_size->meta['convert']['type'] );
571 }
572
573 return $convert_settings['convert_to'];
574 }
575
576 /**
577 * Marks the image as compressed without actually compressing it.
578 *
579 * This method parses existing metadata and delegates to each image size to mark
580 * itself as compressed. It considers conversion settings when marking the sizes.
581 * This is useful for images that are already optimized or when you want to skip
582 * compression while still marking them as processed in the system.
583 *
584 * @since 3.0.0
585 */
586 public function mark_as_compressed() {
587 $this->parse_tiny_metadata();
588
589 $conversion_enabled = $this->settings->get_conversion_enabled();
590
591 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
592
593 if ( $this->settings->get_conversion_enabled() ) {
594 $uncompressed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );
595 $unconverted_sizes = $this->filter_image_sizes( 'unconverted', $active_tinify_sizes );
596
597 $unprocessed_sizes = $uncompressed_sizes + $unconverted_sizes;
598 } else {
599 $unprocessed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );
600 }
601
602 foreach ( $unprocessed_sizes as $size ) {
603 $size->mark_as_compressed( $conversion_enabled );
604 }
605
606 $this->update_tiny_post_meta();
607 }
608 }
609