PluginProbe ʕ •ᴥ•ʔ
TinyPNG – JPEG, PNG & WebP image compression / 3.4.4
TinyPNG – JPEG, PNG & WebP image compression v3.4.4
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 3 years ago config 3 years ago css 3 years ago data 3 years ago images 3 years ago js 3 years ago vendor 2 years ago views 2 years ago class-tiny-bulk-optimization.php 2 years ago class-tiny-compress-client.php 3 years ago class-tiny-compress-fopen.php 3 years ago class-tiny-compress.php 3 years ago class-tiny-exception.php 3 years ago class-tiny-image-size.php 3 years ago class-tiny-image.php 3 years ago class-tiny-notices.php 3 years ago class-tiny-php.php 3 years ago class-tiny-plugin.php 1 year ago class-tiny-settings.php 1 year ago class-tiny-wp-base.php 3 years ago
class-tiny-image.php
479 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 private $settings;
25 private $id;
26 private $name;
27 private $wp_metadata;
28 private $sizes = array();
29 private $statistics = array();
30
31 public function __construct(
32 $settings,
33 $id,
34 $wp_metadata = null,
35 $tiny_metadata = null,
36 $active_sizes = null,
37 $active_tinify_sizes = null
38 ) {
39 $this->settings = $settings;
40 $this->id = $id;
41 $this->wp_metadata = $wp_metadata;
42 $this->parse_wp_metadata();
43 $this->parse_tiny_metadata( $tiny_metadata );
44 $this->detect_duplicates( $active_sizes, $active_tinify_sizes );
45 }
46
47 private function parse_wp_metadata() {
48 if ( ! is_array( $this->wp_metadata ) ) {
49 $this->wp_metadata = wp_get_attachment_metadata( $this->id );
50 }
51 if ( ! is_array( $this->wp_metadata ) ) {
52 return;
53 }
54 if ( ! isset( $this->wp_metadata['file'] ) ) {
55 /* No file metadata found, this might be another plugin messing with
56 metadata. Simply ignore this! */
57 return;
58 }
59
60 $upload_dir = wp_upload_dir();
61 $path_prefix = $upload_dir['basedir'] . '/';
62 $path_info = pathinfo( $this->wp_metadata['file'] );
63 if ( isset( $path_info['dirname'] ) ) {
64 $path_prefix .= $path_info['dirname'] . '/';
65 }
66
67 /* Do not use pathinfo for getting the filename.
68 It doesn't work when the filename starts with a special character. */
69 $path_parts = explode( '/', $this->wp_metadata['file'] );
70 $this->name = end( $path_parts );
71 $filename = $path_prefix . $this->name;
72 $this->sizes[ self::ORIGINAL ] = new Tiny_Image_Size( $filename );
73
74 if ( isset( $this->wp_metadata['sizes'] ) && is_array( $this->wp_metadata['sizes'] ) ) {
75 foreach ( $this->wp_metadata['sizes'] as $size_name => $info ) {
76 $this->sizes[ $size_name ] = new Tiny_Image_Size( $path_prefix . $info['file'] );
77 }
78 }
79 }
80
81 private function detect_duplicates( $active_sizes, $active_tinify_sizes ) {
82 $filenames = array();
83
84 if ( is_array( $this->wp_metadata )
85 && isset( $this->wp_metadata['file'] )
86 && isset( $this->wp_metadata['sizes'] )
87 && is_array( $this->wp_metadata['sizes'] ) ) {
88
89 if ( null == $active_sizes ) {
90 $active_sizes = $this->settings->get_sizes();
91 }
92 if ( null == $active_tinify_sizes ) {
93 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
94 }
95
96 foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
97 if ( $this->sizes[ $size_name ]->has_been_compressed()
98 && array_key_exists( $size_name, $active_sizes ) ) {
99 $filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
100 }
101 }
102 foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
103 if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
104 $filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
105 }
106 }
107 foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
108 if ( array_key_exists( $size_name, $active_sizes ) ) {
109 $filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
110 }
111 }
112 foreach ( $this->wp_metadata['sizes'] as $size_name => $size ) {
113 $filenames = $this->duplicate_check( $filenames, $size['file'], $size_name );
114 }
115 }
116 }
117
118 private function duplicate_check( $filenames, $file, $size_name ) {
119 if ( isset( $filenames[ $file ] ) ) {
120 if ( $filenames[ $file ] != $size_name ) {
121 $this->sizes[ $size_name ]->mark_duplicate( $filenames[ $file ] );
122 }
123 } else {
124 $filenames[ $file ] = $size_name;
125 }
126 return $filenames;
127 }
128
129 private function parse_tiny_metadata( $tiny_metadata ) {
130 if ( is_null( $tiny_metadata ) ) {
131 $tiny_metadata = get_post_meta( $this->id, Tiny_Config::META_KEY, true );
132 }
133 if ( $tiny_metadata ) {
134 foreach ( $tiny_metadata as $size => $meta ) {
135 if ( ! isset( $this->sizes[ $size ] ) ) {
136 if ( self::is_retina( $size ) && Tiny_Settings::wr2x_active() ) {
137 $size_name = rtrim( $size, '_wr2x' );
138 if ( 'original' === $size_name ) {
139 $size_name = '0';
140 }
141 $retina_path = wr2x_get_retina(
142 $this->sizes[ $size_name ]->filename
143 );
144 $this->sizes[ $size ] = new Tiny_Image_Size( $retina_path );
145 } else {
146 $this->sizes[ $size ] = new Tiny_Image_Size();
147 }
148 }
149 $this->sizes[ $size ]->meta = $meta;
150 }
151 }
152 }
153
154 public function get_id() {
155 return $this->id;
156 }
157
158 public function get_name() {
159 return $this->name;
160 }
161
162 public function get_wp_metadata() {
163 return $this->wp_metadata;
164 }
165
166 public function file_type_allowed() {
167 return in_array( $this->get_mime_type(), array( 'image/jpeg', 'image/png', 'image/webp') );
168 }
169
170 public function get_mime_type() {
171 return get_post_mime_type( $this->id );
172 }
173
174 public function download_missing_image_sizes() {
175 global $as3cf;
176
177 if ( ! $as3cf || ! $as3cf->is_plugin_setup() ) {
178 error_log( 'offload s3 plugin not configured..' );
179 return;
180 }
181
182 $s3_data = get_post_meta( $this->id, 'amazonS3_info', true );
183 if ( ! $s3_data ) {
184 return;
185 }
186
187 $path = dirname( $s3_data['key'] );
188
189 foreach ( $this->sizes as $size_name => $size ) {
190 $local_file_path = get_home_path() . $s3_data['key'];
191
192 $s3_data['key'] = wp_normalize_path( $path . '/' . basename( $size->filename ) );
193 $as3cf->plugin_compat->copy_s3_file_to_server( $s3_data, $size->filename );
194 }
195 }
196
197 public function compress() {
198 if ( $this->settings->get_compressor() === null || ! $this->file_type_allowed() ) {
199 return;
200 }
201
202 /* Integration tests need to be written before this can be enabled. */
203 // if ( $this->settings->has_offload_s3_installed() ) {
204 // $this->download_missing_image_sizes();
205 // }
206
207 $success = 0;
208 $failed = 0;
209
210 $compressor = $this->settings->get_compressor();
211 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
212 $uncompressed_sizes = $this->filter_image_sizes( 'uncompressed', $active_tinify_sizes );
213
214 foreach ( $uncompressed_sizes as $size_name => $size ) {
215 if ( ! $size->is_duplicate() ) {
216 $size->add_tiny_meta_start();
217 $this->update_tiny_post_meta();
218 $resize = $this->settings->get_resize_options( $size_name );
219 $preserve = $this->settings->get_preserve_options( $size_name );
220 try {
221 $response = $compressor->compress_file( $size->filename, $resize, $preserve );
222 $size->add_tiny_meta( $response );
223 $success++;
224 } catch ( Tiny_Exception $e ) {
225 $size->add_tiny_meta_error( $e );
226 $failed++;
227 }
228 $this->add_wp_metadata( $size_name, $size );
229 $this->update_tiny_post_meta();
230 }
231 }
232
233 /*
234 Other plugins can hook into this action to execute custom logic
235 after the image sizes have been compressed, ie. cache flushing.
236 */
237 do_action( 'tiny_image_after_compression', $this->id, $success );
238
239 return array(
240 'success' => $success,
241 'failed' => $failed,
242 );
243 }
244
245 public function compress_retina( $size_name, $path ) {
246 if ( $this->settings->get_compressor() === null || ! $this->file_type_allowed() ) {
247 return;
248 }
249
250 if ( ! isset( $this->sizes[ $size_name ] ) ) {
251 $this->sizes[ $size_name ] = new Tiny_Image_Size( $path );
252 }
253 $size = $this->sizes[ $size_name ];
254
255 if ( ! $size->has_been_compressed() ) {
256 $size->add_tiny_meta_start();
257 $this->update_tiny_post_meta();
258 $compressor = $this->settings->get_compressor();
259 $preserve = $this->settings->get_preserve_options( $size_name );
260
261 try {
262 $response = $compressor->compress_file( $path, false, $preserve );
263 $size->add_tiny_meta( $response );
264 } catch ( Tiny_Exception $e ) {
265 $size->add_tiny_meta_error( $e );
266 }
267 $this->update_tiny_post_meta();
268 }
269 }
270
271 public function remove_retina_metadata() {
272 // Remove metadata from all sizes, as this callback only fires when all
273 // retina sizes are deleted.
274 foreach ( $this->sizes as $size_name => $size ) {
275 if ( self::is_retina( $size_name ) ) {
276 unset( $this->sizes[ $size_name ] );
277 }
278 }
279 $this->update_tiny_post_meta();
280 }
281
282 public function add_wp_metadata( $size_name, $size ) {
283 if ( self::is_original( $size_name ) ) {
284 if ( isset( $size->meta['output'] ) ) {
285 $output = $size->meta['output'];
286 if ( isset( $output['width'] ) && isset( $output['height'] ) ) {
287 $this->wp_metadata['width'] = $output['width'];
288 $this->wp_metadata['height'] = $output['height'];
289 $this->wp_metadata['filesize'] = $output['size'];
290 }
291 }
292 }
293 }
294
295 public function update_tiny_post_meta() {
296 $tiny_metadata = array();
297 foreach ( $this->sizes as $size_name => $size ) {
298 $tiny_metadata[ $size_name ] = $size->meta;
299 }
300 update_post_meta( $this->id, Tiny_Config::META_KEY, $tiny_metadata );
301 /*
302 This action is being used by WPML:
303 https://gist.github.com/srdjan-jcc/5c47685cda4da471dff5757ba3ce5ab1
304 */
305 do_action( 'updated_tiny_postmeta', $this->id, Tiny_Config::META_KEY, $tiny_metadata );
306 }
307
308 public function get_image_sizes() {
309 $original = isset( $this->sizes[ self::ORIGINAL ] )
310 ? array(
311 self::ORIGINAL => $this->sizes[ self::ORIGINAL ],
312 )
313 : array();
314 $compressed = array();
315 $uncompressed = array();
316 foreach ( $this->sizes as $size_name => $size ) {
317 if ( self::is_original( $size_name ) ) {
318 continue;
319 }
320
321 if ( $size->has_been_compressed() ) {
322 $compressed[ $size_name ] = $size;
323 } else {
324 $uncompressed[ $size_name ] = $size;
325 }
326 }
327 ksort( $compressed );
328 ksort( $uncompressed );
329 return $original + $compressed + $uncompressed;
330 }
331
332 public function get_image_size( $size = self::ORIGINAL, $create = false ) {
333 if ( isset( $this->sizes[ $size ] ) ) {
334 return $this->sizes[ $size ];
335 } elseif ( $create ) {
336 return new Tiny_Image_Size();
337 } else {
338 return null;
339 }
340 }
341
342 public function filter_image_sizes( $method, $filter_sizes = null ) {
343 $selection = array();
344 if ( is_null( $filter_sizes ) ) {
345 $filter_sizes = array_keys( $this->sizes );
346 }
347 foreach ( $filter_sizes as $size_name ) {
348 if ( ! isset( $this->sizes[ $size_name ] ) ) {
349 continue;
350 }
351
352 $tiny_image_size = $this->sizes[ $size_name ];
353 if ( $tiny_image_size->$method() ) {
354 $selection[ $size_name ] = $tiny_image_size;
355 }
356 }
357 return $selection;
358 }
359
360 public function get_count( $methods, $count_sizes = null ) {
361 $stats = array_fill_keys( $methods, 0 );
362 if ( is_null( $count_sizes ) ) {
363 $count_sizes = array_keys( $this->sizes );
364 }
365 foreach ( $count_sizes as $size ) {
366 if ( ! isset( $this->sizes[ $size ] ) ) {
367 continue;
368 }
369
370 foreach ( $methods as $method ) {
371 if ( $this->sizes[ $size ]->$method() ) {
372 $stats[ $method ]++;
373 }
374 }
375 }
376 return $stats;
377 }
378
379 public function get_latest_error() {
380 $active_tinify_sizes = $this->settings->get_active_tinify_sizes();
381 $error_message = null;
382 $last_timestamp = null;
383 foreach ( $this->sizes as $size_name => $size ) {
384 if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
385 if ( isset( $size->meta['error'] ) && isset( $size->meta['message'] ) ) {
386 if ( null === $last_timestamp || $last_timestamp < $size->meta['timestamp'] ) {
387 $last_timestamp = $size->meta['timestamp'];
388 $error_message = mb_strimwidth( $size->meta['message'], 0 , 140, '...' );
389 }
390 }
391 }
392 }
393 return $error_message;
394 }
395
396 public function get_savings( $stats ) {
397 $before = $stats['initial_total_size'];
398 $after = $stats['optimized_total_size'];
399 if ( 0 === $before ) {
400 $savings = 0;
401 } else {
402 $savings = ($before - $after) / $before * 100;
403 }
404 return '' . number_format( $savings, 1 );
405 }
406
407 public function get_statistics( $active_sizes, $active_tinify_sizes ) {
408 if ( $this->statistics ) {
409 error_log( 'Strangely the image statistics are asked for again.' );
410 return $this->statistics;
411 }
412
413 $this->statistics['initial_total_size'] = 0;
414 $this->statistics['optimized_total_size'] = 0;
415 $this->statistics['image_sizes_optimized'] = 0;
416 $this->statistics['available_unoptimized_sizes'] = 0;
417
418 foreach ( $this->sizes as $size_name => $size ) {
419 if ( ! $size->is_duplicate() ) {
420 if ( array_key_exists( $size_name, $active_sizes ) ) {
421 if ( isset( $size->meta['input'] ) ) {
422 $input = $size->meta['input'];
423 $this->statistics['initial_total_size'] += intval( $input['size'] );
424 if ( isset( $size->meta['output'] ) ) {
425 $output = $size->meta['output'];
426 if ( $size->modified() ) {
427 $this->statistics['optimized_total_size'] += $size->filesize();
428 if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
429 $this->statistics['available_unoptimized_sizes'] += 1;
430 }
431 } else {
432 $this->statistics['optimized_total_size']
433 += intval( $output['size'] );
434 $this->statistics['image_sizes_optimized'] += 1;
435 }
436 } else {
437 $this->statistics['optimized_total_size'] += intval( $input['size'] );
438 }
439 } elseif ( $size->exists() ) {
440 $this->statistics['initial_total_size'] += $size->filesize();
441 $this->statistics['optimized_total_size'] += $size->filesize();
442 if ( in_array( $size_name, $active_tinify_sizes, true ) ) {
443 $this->statistics['available_unoptimized_sizes'] += 1;
444 }
445 }
446 }
447 }
448 }// End foreach().
449
450 /*
451 When an image hasn't yet been optimized but only exists on S3, we still need to
452 know the total size of the image sizes for the bulk optimization tool.
453 TODO: First write integration tests before enabling this again.
454
455 if (
456 0 === $this->statistics['initial_total_size'] &&
457 0 === $this->statistics['optimized_total_size'] &&
458 $this->settings->has_offload_s3_installed()
459 ) {
460 $s3_data = get_post_meta( $this->id, 'wpos3_filesize_total', true );
461 if ( $s3_data ) {
462 $this->statistics['initial_total_size'] = $s3_data;
463 $this->statistics['optimized_total_size'] = $s3_data;
464 }
465 }
466 */
467
468 return $this->statistics;
469 }
470
471 public static function is_original( $size ) {
472 return self::ORIGINAL === $size;
473 }
474
475 public static function is_retina( $size ) {
476 return strrpos( $size, 'wr2x' ) === strlen( $size ) - strlen( 'wr2x' );
477 }
478 }
479