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