PluginProbe ʕ •ᴥ•ʔ
Robin Image Optimizer – Unlimited Image Optimization, WebP & AVIF / trunk
Robin Image Optimizer – Unlimited Image Optimization, WebP & AVIF vtrunk
2.0.5 trunk 1.3.7 1.4.0 1.4.1 1.4.2 1.4.6 1.5.0 1.5.3 1.5.6 1.5.8 1.6.5 1.6.6 1.6.9 1.7.0 1.7.4 1.8.1 1.8.2 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4
robin-image-optimizer / includes / classes / class-rio-attachment.php
robin-image-optimizer / includes / classes Last commit date
models 4 months ago processing 4 months ago processors 4 months ago class-rio-attachment.php 4 months ago class-rio-backup.php 5 months ago class-rio-bulk-optimization.php 5 months ago class-rio-cron.php 6 months ago class-rio-image-query.php 4 months ago class-rio-image-statistic.php 4 months ago class-rio-media-library.php 4 months ago class-rio-multisite.php 6 months ago class-rio-optimization-orchestrator.php 4 months ago class-rio-optimization-tools.php 6 months ago class-rio-views.php 4 months ago class-wrio-license.php 6 months ago class-wrio-premium-provider.php 6 months ago class-wrio-support.php 6 months ago index.php 6 months ago
class-rio-attachment.php
1132 lines
1 <?php
2
3 // Exit if accessed directly
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * Класс для работы с WordPress attachment.
10 *
11 * @version 1.0
12 */
13 class WIO_Attachment {
14
15 /**
16 * @var int
17 */
18 private $id;
19
20
21 /**
22 * @var array meta-данные
23 */
24 private $attachment_meta;
25
26 /**
27 * @var array массив с данными о папке uploads
28 */
29 private $wp_upload_dir;
30
31 /**
32 * @var string
33 */
34 private $url;
35
36 /**
37 * @var string
38 */
39 private $path;
40
41 /**
42 * @var RIO_Process_Queue
43 */
44 private $optimization_data;
45
46 /**
47 * Инициализация аттачмента
48 *
49 * @param int $attachment_id Номер аттачмента из медиабиблиотеки
50 * @param array|false $attachment_meta метаданные аттачмента. Ключи массива аналогичны функции wp_get_attachment_metadata
51 */
52 public function __construct( $attachment_id, $attachment_meta = false ) {
53 $this->id = $attachment_id;
54 $this->wp_upload_dir = wp_upload_dir();
55 $this->attachment_meta = $attachment_meta;
56
57 if ( ! $attachment_meta ) {
58 // some meta can be missing due to: https://wordpress.stackexchange.com/q/330174/149161
59 $this->attachment_meta = wp_get_attachment_metadata( $this->id );
60 }
61
62 $this->set_paths();
63 }
64
65 /**
66 * @return bool
67 * @since 1.3.9
68 */
69 public function isset_attachment_meta() {
70 return $this->attachment_meta && isset( $this->attachment_meta['file'] );
71 }
72
73 /**
74 * @since 1.3.9
75 */
76 public function set_paths() {
77 if ( ! $this->isset_attachment_meta() ) {
78 return;
79 }
80
81 $this->url = trailingslashit( $this->wp_upload_dir['baseurl'] ) . $this->attachment_meta['file'];
82 $this->path = wp_normalize_path( trailingslashit( $this->wp_upload_dir['basedir'] ) . $this->attachment_meta['file'] );
83 }
84
85 /**
86 * Актуализирует мета данные аттачмента и загружает актуальные мета данные и данные по оптимизации из базы.
87 */
88 public function reload( $attachment_meta = [] ) {
89 if ( empty( $attachment_meta ) ) {
90 $attachment_meta = wp_get_attachment_metadata( $this->id );
91 }
92 $this->attachment_meta = $attachment_meta;
93 $this->optimization_data = new RIO_Process_Queue(
94 [
95 'object_id' => $this->id,
96 'object_name' => '',
97 'item_type' => 'attachment',
98 ]
99 );
100 $this->optimization_data->load();
101 $this->set_paths();
102 }
103
104 /**
105 * Fallback to get attachment meta it can be empty when WordPress failed to create it or invocation
106 * of method was produced too soon.
107 *
108 * @return bool
109 * @since 1.3.9
110 */
111 public function regenerate_metadata() {
112 if ( $this->isset_attachment_meta() ) {
113 return true;
114 }
115
116 WRIO_Plugin::app()->logger->info( sprintf( 'Try regenerate metadata for attachment #%d', $this->id ) );
117
118 // Need to remove this filter, as it would start recursion
119 remove_filter( 'wp_generate_attachment_metadata', 'WRIO_Media_Library::optimize_after_upload' );
120
121 $file_path = get_attached_file( $this->id );
122
123 if ( empty( $file_path ) ) {
124 $attachment = get_post( $this->id );
125
126 if ( empty( $attachment ) || 'attachment' !== $attachment->post_type ) {
127 return false;
128 }
129
130 $file_path = wrio_url_to_abs_path( $attachment->guid );
131 }
132
133 if ( empty( $file_path ) || ! file_exists( $file_path ) ) {
134 WRIO_Plugin::app()->logger->info( sprintf( 'Failed regenerate attachment meta data. Attachment file (%s) doesn\'t exists!', $file_path ) );
135
136 return false;
137 }
138
139 if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
140 require ABSPATH . 'wp-admin/includes/image.php';
141 }
142 $attachment_meta = wp_generate_attachment_metadata( $this->id, $file_path );
143
144 if ( empty( $attachment_meta ) ) {
145 WRIO_Plugin::app()->logger->error( sprintf( 'Failed regenerate meta data for attachment file (%s).', $file_path ) );
146
147 return false;
148 }
149
150 WRIO_Plugin::app()->logger->debug( sprintf( 'Generated metadata: %s', var_export( $attachment_meta, true ) ) );
151
152 // Updating metadata in database
153 wp_update_attachment_metadata( $this->id, $attachment_meta );
154
155 $this->reload( $attachment_meta );
156
157 add_filter( 'wp_generate_attachment_metadata', 'WRIO_Media_Library::optimize_after_upload', 10, 2 );
158
159 WRIO_Plugin::app()->logger->info( sprintf( 'Finish regenerate metadata for attachment #%d!', $this->id ) );
160
161 return true;
162 }
163
164 /**
165 * Добавляем сообщение в лог файл.
166 *
167 * @param string $message Текст сообщения об ошибке.
168 */
169 public function writeLog( $message ) {
170
171 $char = "\t-> ";
172 $nl = PHP_EOL;
173
174 $error = sprintf( 'Error to optimize attachment (ID: #%s). Message: "%s"', $this->id, trim( $message ) ) . $nl;
175 $error .= $char . sprintf( 'Attachment optimized? %s', ( $this->isOptimized() ? 'Yes' : 'No' ) ) . $nl;
176 $error .= $char . sprintf( 'Should be resized? %s', ( $this->isNeedResize() ? 'Yes' : 'No' ) ) . $nl;
177 $error .= $char . sprintf( 'Original size: %sx%s', $this->attachment_meta['width'], $this->attachment_meta['height'] ) . $nl;
178 $error .= $char . sprintf( 'Relative path: %s', $this->attachment_meta['file'] ) . $nl;
179 $error .= $char . sprintf( 'Server used: %s', wrio_is_license_activate() ? 'premium' : 'free' ) . $nl;
180
181 if ( ! empty( $this->attachment_meta['sizes'] ) ) {
182 $error .= $char . ' Additional sizes:' . $nl;
183 foreach ( $this->attachment_meta['sizes'] as $size_type => $size_info ) {
184 $error .= "\t" . $char . sprintf( 'Type: %s, size: %sx%s, MIME type: %s', $size_type, $size_info['width'], $size_info['height'], $size_info['mime-type'] ) . $nl;
185 }
186 }
187
188 WRIO_Plugin::app()->logger->error( $error );
189 }
190
191 /**
192 * Возвращает объект с информацией об оптимизации
193 *
194 * @return RIO_Process_Queue
195 */
196 public function getOptimizationData() {
197 $this->optimization_data = new RIO_Process_Queue(
198 [
199 'object_id' => $this->id,
200 'object_name' => '',
201 'item_type' => 'attachment',
202 ]
203 );
204 $this->optimization_data->load();
205
206 return $this->optimization_data;
207 }
208
209 /**
210 * Возвращает объект с информацией о конвертации в указанный формат
211 *
212 * @param string $format Format to get conversion data for ('webp' or 'avif')
213 *
214 * @return RIO_Process_Queue
215 */
216 public function getConversionData( $format = 'webp' ) {
217 $optimization_data = new RIO_Process_Queue(
218 [
219 'object_id' => $this->id,
220 'object_name' => '',
221 'item_type' => $format,
222 ]
223 );
224 $optimization_data->load();
225
226 return $optimization_data;
227 }
228
229 /**
230 * Read file size reliably within the current request.
231 * PHP caches stat() results; we clear cache for this path.
232 *
233 * @param mixed $file_path The file path.
234 *
235 * @return int
236 */
237 protected function get_file_size( $file_path ) {
238 return wrio_get_file_size( $file_path );
239 }
240
241 /**
242 * Оптимизация аттачмента.
243 *
244 * @param string $optimization_level Уровень оптимизации изображения.
245 *
246 * @return array
247 */
248 public function optimize( $optimization_level = '' ) {
249 $optimize_results = [
250 'original_size' => 0,
251 'optimized_size' => 0,
252 ];
253
254 if ( empty( $optimization_level ) ) {
255 $optimization_level = WRIO_Plugin::app()->getPopulateOption( 'image_optimization_level', 'normal' );
256 }
257
258 if ( $optimization_level === 'custom' ) {
259 $custom_quality = WRIO_Plugin::app()->getPopulateOption( 'image_optimization_level_custom', 100 );
260 $optimization_level = intval( $custom_quality );
261 }
262
263 $optimization_data = $this->getOptimizationData();
264 $results = [
265 'original_size' => 0,
266 'final_size' => 0,
267 'original_mime_type' => '',
268 'final_mime_type' => '',
269 ];
270 $results['processing_level'] = $optimization_level;
271
272 try {
273 // The path may be empty because no metadata has been created for the image.
274 // We should try to create image metadata again.
275 if ( ! $this->isset_attachment_meta() ) {
276 WRIO_Plugin::app()->logger->warning( sprintf( 'Attachment #%d doesn\'t have metadata.', $this->id ) );
277
278 $this->regenerate_metadata();
279 }
280
281 if ( empty( $this->path ) || ! file_exists( $this->path ) ) {
282 $results['result_status'] = 'error';
283
284 $error_message = __( 'Attachment cannot be optimized.', 'robin-image-optimizer' );
285
286 if ( empty( $this->path ) ) {
287 // translators: %d is the attachment ID
288 $error_message .= ' ' . sprintf( __( 'Attachment #%d doesn\'t have metadata, the image may be damaged.', 'robin-image-optimizer' ), $this->id );
289 } else {
290 // translators: %s is the file path
291 $error_message .= ' ' . sprintf( __( 'File "(%s)" doesn\'t exist', 'robin-image-optimizer' ), $this->path );
292 }
293
294 $extra_data = [
295 'error' => 'path',
296 'error_msg' => $error_message,
297 ];
298
299 $results['extra_data'] = new RIO_Attachment_Extra_Data( $extra_data );
300 $optimization_data->configure( $results );
301 $optimization_data->save();
302
303 WRIO_Plugin::app()->logger->error( sprintf( 'Failed to find original attachment #%s located in %s. Skipping optimization. This may be caused due to bug in %s function, which returns false for attachment meta', $this->id, empty( $this->path ) ? '*empty path*' : $this->path, 'wp_get_attachment_metadata()' ) );
304
305 return $optimize_results;
306 }
307
308 // сначала бекапим
309 $is_image_backuped = $this->backup();
310
311 if ( is_wp_error( $is_image_backuped ) ) {
312 $error_msg = $is_image_backuped->get_error_message();
313 $this->writeLog( $error_msg );
314
315 $results['result_status'] = 'error';
316 $extra_data = [
317 'error' => 'backup',
318 'error_msg' => 'Failed to backup',
319 ];
320 $results['extra_data'] = new RIO_Attachment_Extra_Data( $extra_data );
321 $optimization_data->configure( $results );
322 $optimization_data->save();
323
324 WRIO_Plugin::app()->logger->error( sprintf( 'Failed to make backup of original attachment #%s. Skipping optimization.', $this->id ) );
325
326 return $optimize_results;
327 }
328
329 $results['is_backed_up'] = $is_image_backuped;
330
331 $original_main_size = $this->get_file_size( $this->path );
332
333 // если файл большой - изменяем размер
334 if ( $this->isNeedResize() ) {
335 $this->resize();
336 }
337
338 $image_processor = WIO_OptimizationTools::getImageProcessor();
339
340 clearstatcache(); // на всякий случай очистим кеш файловой статистики
341
342 $optimized_img_data = $image_processor->process(
343 [
344 'image_url' => $this->get( 'url' ),
345 'image_path' => $this->get( 'path' ),
346 'quality' => $image_processor->quality( $optimization_level ),
347 'save_exif' => WRIO_Plugin::app()->getPopulateOption( 'save_exif_data', false ),
348 'is_thumb' => false,
349 ]
350 );
351
352 // проверяем на ошибку
353 if ( is_wp_error( $optimized_img_data ) ) {
354 $error_msg = $optimized_img_data->get_error_message();
355 $this->writeLog( $error_msg );
356
357 $results['result_status'] = 'error';
358
359 $extra_data = [
360 'error' => 'optimization',
361 'error_msg' => $error_msg,
362 ];
363
364 $results['extra_data'] = new RIO_Attachment_Extra_Data( $extra_data );
365
366 $optimization_data->configure( $results );
367 $optimization_data->save();
368
369 WRIO_Plugin::app()->logger->error( sprintf( 'Failed to process (url: %s, path: %s, quality: %s) as error was returned: %s', $this->get( 'url' ), $this->get( 'path' ), $image_processor->quality( $optimization_level ), $error_msg ) );
370
371 return $optimize_results;
372 }
373
374 $results['original_mime_type'] = '';
375 $results['final_mime_type'] = '';
376
377 // отложенная оптимизация
378 if ( isset( $optimized_img_data['status'] ) && 'processing' === $optimized_img_data['status'] ) {
379 $results['result_status'] = 'processing';
380 $results['original_size'] = 0;
381 $results['final_size'] = 0;
382
383 $extra_data = [
384 'original_main_size' => $original_main_size,
385 'main_optimized_data' => $optimized_img_data,
386 'thumbnails_optimized_data' => $this->optimizeImageSizes(),
387 ];
388
389 $results['extra_data'] = new RIO_Attachment_Extra_Data( $extra_data );
390
391 $optimization_data->configure( $results );
392 $optimization_data->save();
393 $optimize_results['processing'] = 1;
394
395 return $optimize_results;
396 }
397
398 // скачиваем и заменяем главную картинку
399 $image_downloaded = $this->replaceOriginalFile( $optimized_img_data );
400
401 // некоторые провайдеры не отдают оптимизированный размер, поэтому после замены файла получаем его сами
402 if ( ! $optimized_img_data['optimized_size'] ) {
403 clearstatcache();
404 $optimized_img_data['optimized_size'] = $this->get_file_size( $this->get( 'path' ) );
405 }
406
407 // при отрицательной оптимизации ставим значение оригинала
408 if ( $optimized_img_data['optimized_size'] > $original_main_size ) {
409 $optimized_img_data['optimized_size'] = $original_main_size;
410 }
411
412 if ( $image_downloaded ) {
413 // просчитываем статистику
414 $optimize_results['original_size'] += $original_main_size;
415 $optimize_results['optimized_size'] += $optimized_img_data['optimized_size'];
416 $thumbnails_count = 0;
417
418 // оптимизируем дополнительные размеры
419 $optimized_img_sizes_data = $this->optimizeImageSizes();
420
421 // добавляем к статистике данные по оптимизации доп размеров
422 if ( ! empty( $optimized_img_sizes_data ) ) {
423 $optimize_results['original_size'] += $optimized_img_sizes_data['original_size'];
424 $optimize_results['optimized_size'] += $optimized_img_sizes_data['optimized_size'];
425 $thumbnails_count = $optimized_img_sizes_data['thumbnails_count'];
426 }
427
428 $results['result_status'] = 'success';
429 $results['final_size'] = $optimize_results['optimized_size'];
430 $results['original_size'] = $optimize_results['original_size'];
431
432 $extra_data = [
433 'thumbnails_count' => $thumbnails_count,
434 'original_main_size' => $original_main_size,
435 ];
436
437 $results['extra_data'] = new RIO_Attachment_Extra_Data( $extra_data );
438 $mime_type = '';
439
440 if ( function_exists( 'wp_get_image_mime' ) ) {
441 $mime_type = wp_get_image_mime( $this->get( 'path' ) );
442 } else {
443 WRIO_Plugin::app()->logger->error( 'App is missing wp_get_image_mime() function, unable to get MIME type' );
444 }
445
446 $results['original_mime_type'] = $mime_type;
447 $results['final_mime_type'] = $mime_type;
448 $optimization_data->configure( $results );
449 } else {
450 $error_msg = 'Failed to get optimized image from remote server';
451 $this->writeLog( $error_msg );
452
453 $results['result_status'] = 'error';
454
455 $extra_data = [
456 'error' => 'download',
457 'error_msg' => $error_msg,
458 ];
459
460 $results['extra_data'] = new RIO_Attachment_Extra_Data( $extra_data );
461 $optimization_data->configure( $results );
462 }
463
464 $optimization_data->save();
465
466 return $optimize_results;
467 } catch ( Throwable $throwable ) {
468 $this->mark_and_log_failure( $throwable, 'optimization', $optimization_level );
469 }
470
471 return $optimize_results;
472 }
473
474 /**
475 * Log and persist unexpected optimization failures.
476 *
477 * @param Throwable $throwable Exception or error that was thrown.
478 * @param string $context Processing context.
479 * @param string|int $processing_level Current processing level.
480 *
481 * @return void
482 */
483 public function mark_and_log_failure( $throwable, $context = 'optimization', $processing_level = '' ) {
484 $error_message = sprintf(
485 'Unexpected %1$s failure for attachment #%2$d: %3$s in %4$s:%5$d',
486 $context,
487 $this->id,
488 $throwable->getMessage(),
489 $throwable->getFile(),
490 $throwable->getLine()
491 );
492
493 WRIO_Plugin::app()->logger->error( $error_message );
494
495 $optimization_data = $this->getOptimizationData();
496 $optimization_data->mark_as_error(
497 $error_message,
498 [
499 'processing_level' => $processing_level,
500 'original_size' => 0,
501 'final_size' => 0,
502 'original_mime_type' => '',
503 'final_mime_type' => '',
504 ]
505 );
506 }
507
508 /**
509 * Log and persist unexpected format conversion failures without rewriting attachment optimization state.
510 *
511 * @param Throwable $throwable Exception or error that was thrown.
512 * @param string $format Target format.
513 * @param string $context Processing context.
514 *
515 * @return void
516 */
517 public function mark_conversion_failure( $throwable, $format, $context = 'conversion' ) {
518 $error_message = sprintf(
519 'Unexpected %1$s failure for attachment #%2$d (%3$s): %4$s in %5$s:%6$d',
520 $context,
521 $this->id,
522 $format,
523 $throwable->getMessage(),
524 $throwable->getFile(),
525 $throwable->getLine()
526 );
527
528 WRIO_Plugin::app()->logger->error( $error_message );
529
530 $conversion_data = $this->getConversionData( $format );
531 if ( ! $conversion_data->get_id() ) {
532 return;
533 }
534
535 $conversion_data->mark_as_error( $error_message );
536 }
537
538 /**
539 * Отложенная оптимизация аттачмента
540 *
541 * @return bool|array
542 */
543 public function deferredOptimization() {
544 $results = [
545 'original_size' => 0,
546 'optimized_size' => 0,
547 'optimized_count' => 0,
548 'processing' => 1,
549 ];
550
551 $image_processor = WIO_OptimizationTools::getImageProcessor();
552 $optimization_data = $this->getOptimizationData();
553
554 if ( $optimization_data->get_result_status() !== 'processing' ) {
555 return false;
556 }
557
558 // проверяем главную картинку
559 /**
560 * @var RIO_Attachment_Extra_Data $extra_data
561 */
562 $extra_data = $optimization_data->get_extra_data();
563 $main_optimized_data = $extra_data->get_main_optimized_data();
564 $main_image_url = '';
565
566 if ( ! $main_optimized_data['optimized_img_url'] ) {
567 $main_image_url = $image_processor->checkDeferredOptimization( $main_optimized_data );
568 if ( $main_image_url ) {
569 $main_optimized_data['optimized_img_url'] = $main_image_url;
570 $extra_data->set_main_optimized_data( $main_optimized_data );
571 }
572 }
573
574 $thumbnails_processed = true;
575 $thumbnails = (array) $extra_data->get_thumbnails_optimized_data();
576 $thumbnails = json_decode( json_encode( $thumbnails ), true ); // рекурсивная конвертация объекта в массив
577
578 if ( is_array( $thumbnails['thumbnails'] ) ) {
579 foreach ( $thumbnails['thumbnails'] as &$thumbnail_optimized_data ) {
580 if ( ! $thumbnail_optimized_data['optimized_img_url'] ) {
581 $thumbnail_image_url = $image_processor->checkDeferredOptimization( $thumbnail_optimized_data );
582 if ( $thumbnail_image_url ) {
583 $thumbnail_optimized_data['optimized_img_url'] = $thumbnail_image_url;
584 } else {
585 $thumbnails_processed = false;
586 }
587 }
588 }
589 $extra_data->set_thumbnails_optimized_data( $thumbnails );
590 }
591
592 // когда все файлы получены - со�
593 раняем и возвращаем результат
594 if ( $main_image_url && $thumbnails_processed ) {
595 $original_size = 0;
596 $optimized_size = 0;
597 $thumbnails_count = 0;
598 $original_main_size = (int) $extra_data->get_original_main_size();
599 if ( $original_main_size <= 0 ) {
600 $original_main_size = $this->get_file_size( $this->get( 'path' ) );
601 }
602 $original_size = $original_size + $original_main_size;
603
604 $this->replaceOriginalFile(
605 [
606 'optimized_img_url' => $main_image_url,
607 ]
608 );
609
610 clearstatcache();
611
612 $optimized_main_size = $this->get_file_size( $this->get( 'path' ) );
613
614 // при отрицательной оптимизации ставим значение оригинала
615 if ( $optimized_main_size > $original_main_size ) {
616 $optimized_main_size = $original_main_size;
617 }
618
619 $optimized_size = $optimized_size + $optimized_main_size;
620
621 if ( is_array( $thumbnails['thumbnails'] ) ) {
622 foreach ( $thumbnails['thumbnails'] as $thumbnail_size => $thumbnail ) {
623 $thumbnail_file = $this->getImageSizePath( $thumbnail_size );
624 $original_thumbnail_size = $this->get_file_size( $thumbnail_file );
625 $original_size = $original_size + $original_thumbnail_size;
626
627 $this->replaceOriginalFile(
628 [
629 'optimized_img_url' => $thumbnail['optimized_img_url'],
630 ],
631 $thumbnail_size
632 );
633
634 clearstatcache();
635
636 $optimized_thumbnail_size = $this->get_file_size( $thumbnail_file );
637
638 // при отрицательной оптимизации ставим значение оригинала
639 if ( $optimized_thumbnail_size > $original_thumbnail_size ) {
640 $optimized_thumbnail_size = $original_thumbnail_size;
641 }
642
643 $optimized_size = $optimized_size + $optimized_thumbnail_size;
644
645 ++$thumbnails_count;
646 }
647 }
648
649 $mime_type = '';
650 if ( function_exists( 'wp_get_image_mime' ) ) {
651 $mime_type = wp_get_image_mime( $this->get( 'path' ) );
652 }
653
654 $optimization_data->configure(
655 [
656 'final_size' => $optimized_size,
657 'original_size' => $original_size,
658 'result_status' => 'success',
659 'original_mime_type' => $mime_type,
660 'final_mime_type' => $mime_type,
661 ]
662 );
663
664 $extra_data->set_original_main_size( $original_main_size );
665 $extra_data->set_thumbnails_count( $thumbnails_count );
666
667 // удаляем промежуточные данные
668 $extra_data->set_main_optimized_data( null );
669 $extra_data->set_thumbnails_optimized_data( null );
670 $extra_data->set_main_optimized_data( null );
671
672 $results['optimized_count'] = 1;
673 $results['original_size'] = $original_size;
674 $results['optimized_size'] = $optimized_size;
675
676 unset( $results['processing'] );
677 }
678 $optimization_data->set_extra_data( $extra_data );
679 $optimization_data->save();
680
681 return $results;
682 }
683
684 /**
685 * Метод проверяет, оптимизирован ли аттачмент
686 *
687 * @return bool
688 */
689 public function isOptimized() {
690 $optimization_data = $this->getOptimizationData();
691 if ( $optimization_data->is_optimized() ) {
692 return true;
693 }
694
695 return false;
696 }
697
698 /**
699 * Возвращает все размеры аттачмента, которые нужно оптимизировать
700 *
701 * @return array|false
702 */
703 public function getAllowedSizes() {
704 $allowed_sizes = WRIO_Plugin::app()->getPopulateOption( 'allowed_sizes_thumbnail', 'thumbnail,medium' );
705
706 if ( ! $allowed_sizes ) {
707 return false;
708 }
709
710 $allowed_sizes = explode( ',', $allowed_sizes );
711
712 return $allowed_sizes;
713 }
714
715 /**
716 * Оптимизация други�
717 размеров аттачмента.
718 *
719 * @return array
720 */
721 public function optimizeImageSizes() {
722 $allowed_sizes = $this->getAllowedSizes();
723
724 if ( $allowed_sizes === false ) {
725 return [];
726 }
727
728 $image_processor = WIO_OptimizationTools::getImageProcessor();
729 $quality = WRIO_Plugin::app()->getPopulateOption( 'image_optimization_level', 'normal' );
730
731 if ( $quality === 'custom' ) {
732 $custom_quality = WRIO_Plugin::app()->getPopulateOption( 'image_optimization_level_custom', 100 );
733 $quality = intval( $custom_quality );
734 }
735
736 $exif = WRIO_Plugin::app()->getPopulateOption( 'save_exif_data', false );
737
738 $original_size = 0;
739 $optimized_size = 0;
740 $errors_count = 0;
741 $optimized_count = 0;
742 $thumbnails = [];
743
744 foreach ( $allowed_sizes as $image_size ) {
745 $url = $this->getImageSizeUrl( $image_size );
746 $path = $this->getImageSizePath( $image_size );
747
748 if ( ! $url || ! $path ) {
749 continue;
750 }
751
752 $original_file_size = 0;
753
754 if ( is_file( $path ) ) {
755 $original_file_size = $this->get_file_size( $path );
756 }
757
758 $optimized_img_data = $image_processor->process(
759 [
760 'image_url' => $url,
761 'image_path' => $path,
762 'quality' => $image_processor->quality( $quality ),
763 'save_exif' => $exif,
764 'is_thumb' => true,
765 ]
766 );
767 // проверяем на ошибку
768 if ( is_wp_error( $optimized_img_data ) ) {
769 ++$errors_count;
770 } else {
771 // скачиваем и заменяем картинку
772 $this->replaceOriginalFile( $optimized_img_data, $image_size );
773 // некоторые провайдеры не отдают оптимизированный размер, поэтому после замены файла получаем его сами
774 if ( ! $optimized_img_data['optimized_size'] ) {
775 clearstatcache();
776 $optimized_img_data['optimized_size'] = $this->get_file_size( $path );
777 }
778 if ( ! $optimized_img_data['src_size'] ) {
779 $optimized_img_data['src_size'] = $original_file_size;
780 }
781
782 // при отрицательной оптимизации ставим значение оригинала
783 if ( $optimized_img_data['optimized_size'] > $original_file_size ) {
784 $optimized_img_data['optimized_size'] = $original_file_size;
785 }
786
787 $thumbnails[ $image_size ] = $optimized_img_data;
788
789 // просчитываем статистику
790 $original_size += $optimized_img_data['src_size'];
791 $optimized_size += $optimized_img_data['optimized_size'];
792 ++$optimized_count;
793 }
794 }
795
796 return [
797 'errors_count' => $errors_count,
798 'original_size' => $original_size,
799 'optimized_size' => $optimized_size,
800 'thumbnails_count' => $optimized_count,
801 'thumbnails' => $thumbnails,
802 ];
803 }
804
805 /**
806 * Возвращает путь.
807 *
808 * @param string $image_size Размер(thumbnail, medium ... )
809 *
810 * @return string
811 */
812 public function getPath( $image_size = '' ) {
813
814 if ( empty( $image_size ) ) {
815 $path = $this->path;
816 } else {
817 $path = $this->getImageSizePath( $image_size );
818 }
819
820 return $path;
821 }
822
823 /**
824 * Заменяет оригинальный файл на оптимизированный.
825 *
826 * @param array $optimized_img_data Hезультат оптимизации ввиде массива данны�
827 .
828 * @param string $image_size Размер (thumbnail, medium ... )
829 *
830 * @return bool
831 */
832 public function replaceOriginalFile( $optimized_img_data, $image_size = '' ) {
833
834 $optimized_img_url = $optimized_img_data['optimized_img_url'];
835
836 if ( isset( $optimized_img_data['not_need_download'] ) && (bool) $optimized_img_data['not_need_download'] ) {
837 $optimized_file = $optimized_img_url;
838 } else {
839 $optimized_file = $this->remoteDownloadImage( $optimized_img_url );
840 }
841
842 if ( empty( $optimized_file ) ) {
843 WRIO_Plugin::app()->logger->error( sprintf( 'Unable to replace original image with new as failed to download %s', $optimized_img_url ) );
844
845 return false;
846 }
847
848 if ( isset( $optimized_img_data['not_need_replace'] ) && $optimized_img_data['not_need_replace'] ) {
849 // если картинка уже оптимизирована и провайдер её не может уменьшить - он может вернуть положительный ответ, но без самой картинки. В таком случае ничего заменять не надо
850 return true;
851 }
852
853 $attachment_size_path = $this->getPath( $image_size );
854
855 if ( ! is_file( $attachment_size_path ) ) {
856 return false;
857 }
858
859 $bytes = @file_put_contents( $attachment_size_path, $optimized_file );
860
861 if ( $bytes === false ) {
862 WRIO_Plugin::app()->logger->error( sprintf( 'Failed to put new image\'s %s content to %s as file_put_contents() failed', $optimized_img_url, $attachment_size_path ) );
863
864 return false;
865 }
866
867 return true;
868 }
869
870 /**
871 * Скачивание изображения с удалённого сервера
872 *
873 * @param string $url
874 *
875 * @return string|null Image content on success, NULL on failure.
876 */
877 protected function remoteDownloadImage( $url ) {
878 $user_agent = wrio_get_user_agent();
879
880 if ( ! function_exists( 'curl_version' ) ) {
881 $context_options = [
882 'ssl' => [
883 'verify_peer' => false,
884 'verify_peer_name' => false,
885 ],
886 'http' => [
887 'header' => 'User-Agent: ' . $user_agent,
888 ],
889 ];
890
891 $content = @file_get_contents( $url, false, stream_context_create( $context_options ) );
892
893 if ( $content === false ) {
894 WRIO_Plugin::app()->logger->error( sprintf( 'Failed to get content of "%s" using file_get_contents()', $url ) );
895
896 return null;
897 }
898
899 return $content;
900 }
901
902 $ch = curl_init();
903 curl_setopt( $ch, CURLOPT_HEADER, 0 );
904 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
905 curl_setopt( $ch, CURLOPT_URL, $url );
906 curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 0 );
907 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0 );
908 curl_setopt( $ch, CURLOPT_USERAGENT, $user_agent );
909
910 $image_body = curl_exec( $ch );
911 $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
912 if ( $http_code !== 200 ) {
913 $image_body = false;
914 }
915 curl_close( $ch );
916
917 if ( $image_body === false ) {
918 WRIO_Plugin::app()->logger->error( sprintf( 'Failed to get content of "%s" using curl_exec(). HTTP code: ' . $http_code, $url ) );
919
920 return null;
921 }
922
923 return $image_body;
924 }
925
926 /**
927 * Возвращает свойство аттачмента
928 *
929 * @param string $property имя свойства
930 *
931 * @return mixed
932 */
933 public function get( $property ) {
934 if ( isset( $this->$property ) ) {
935 return $this->$property;
936 }
937
938 return false;
939 }
940
941 /**
942 * Возвращает URL изображения по указанному размеру
943 *
944 * @param string $size - размер изображения(thumbnail,medium,large...)
945 *
946 * @return string|null
947 */
948 public function getImageSizeUrl( $size = 'thumbnail' ) {
949 if ( ! isset( $this->attachment_meta['sizes'][ $size ] ) ) {
950 return null;
951 }
952
953 $file = $this->attachment_meta['sizes'][ $size ]['file'];
954 $url = str_replace( wp_basename( $this->url ), $file, $this->url );
955
956 return $url;
957 }
958
959 /**
960 * Возвращает путь к изображению по указанному размеру.
961 *
962 * @param string $size Размер изображения (thumbnail, medium, large ...)
963 *
964 * @return string Путь до изображения.
965 */
966 public function getImageSizePath( $size = 'thumbnail' ) {
967 if ( ! isset( $this->attachment_meta['sizes'][ $size ] ) ) {
968 return null;
969 }
970
971 $file = $this->attachment_meta['sizes'][ $size ]['file'];
972 $path = str_replace( wp_basename( $this->path ), $file, $this->path );
973
974 return $path;
975 }
976
977 /**
978 * Проверка необ�
979 одимости делать изменение размера.
980 *
981 * @return bool
982 */
983 protected function isNeedResize() {
984 $resize_large_images = WRIO_Plugin::app()->getPopulateOption( 'resize_larger', true );
985
986 if ( ! $resize_large_images ) {
987 return false;
988 }
989
990 $resize_larger_w = (int) WRIO_Plugin::app()->getPopulateOption( 'resize_larger_w', 1600 );
991 $resize_larger_h = (int) WRIO_Plugin::app()->getPopulateOption( 'resize_larger_h', 1600 );
992
993 if ( ! $resize_larger_w && ! $resize_larger_h ) {
994 return false;
995 }
996
997 // если ширина и высота установлены и > 0
998 if ( $this->attachment_meta['width'] >= $this->attachment_meta['height'] ) {
999 $larger_side = $this->attachment_meta['width'];
1000 $resize_larger_side = $resize_larger_w;
1001 } else {
1002 $larger_side = $this->attachment_meta['height'];
1003 $resize_larger_side = $resize_larger_h;
1004 }
1005 // если ширина 0, то рисайзим по высоте
1006 if ( ! $resize_larger_w ) {
1007 $resize_larger_side = $resize_larger_h;
1008 $larger_side = $this->attachment_meta['height'];
1009 }
1010 // если высота 0, то рисайзим по ширине
1011 if ( ! $resize_larger_h ) {
1012 $resize_larger_side = $resize_larger_w;
1013 $larger_side = $this->attachment_meta['width'];
1014 }
1015 // если большая сторона картинки меньше, чем задано в настройка�
1016 , то не рисайзим.
1017 if ( $larger_side <= $resize_larger_side ) {
1018 return false;
1019 }
1020
1021 return true;
1022 }
1023
1024 /**
1025 * Возвращает метаданные аттачмента
1026 *
1027 * @return array
1028 */
1029 public function getMetaData() {
1030 return $this->attachment_meta;
1031 }
1032
1033 /**
1034 * Изменяет размер изображения до заданного в настройка�
1035 размера.
1036 *
1037 * @return bool
1038 */
1039 protected function resize() {
1040 $resize_larger_h = (int) WRIO_Plugin::app()->getPopulateOption( 'resize_larger_h', 1600 );
1041 $resize_larger_w = (int) WRIO_Plugin::app()->getPopulateOption( 'resize_larger_w', 1600 );
1042
1043 $image = wp_get_image_editor( $this->path );
1044
1045 if ( is_wp_error( $image ) ) {
1046 WRIO_Plugin::app()->logger->error( sprintf( 'Failed to get image edit via wp_get_image_editor(), error: "%s"', $image->get_error_message() ) );
1047
1048 return false;
1049 }
1050
1051 $current_size = $image->get_size();
1052 $new_width = 0;
1053 $new_height = 0;
1054
1055 // если обе стороны заданы
1056 if ( $resize_larger_h && $resize_larger_w ) {
1057 // определяем большую сторону и по ней маштабируем
1058 if ( $current_size['width'] >= $current_size['height'] ) {
1059 $new_width = $resize_larger_w;
1060 $new_height = round( $current_size['height'] * $new_width / $current_size['width'] );
1061 } else {
1062 $new_height = $resize_larger_h;
1063 $new_width = round( $current_size['width'] * $new_height / $current_size['height'] );
1064 }
1065 } else {
1066 // если задана одна из сторон
1067 if ( ! $resize_larger_w ) {
1068 // если ширина 0, то рисайзим по высоте
1069 $new_height = $resize_larger_h;
1070 $new_width = round( $current_size['width'] * $new_height / $current_size['height'] );
1071 }
1072 if ( ! $resize_larger_h ) {
1073 // если высота 0, то рисайзим по ширине
1074 $new_width = $resize_larger_w;
1075 $new_height = round( $current_size['height'] * $new_width / $current_size['width'] );
1076 }
1077 }
1078
1079 $nl = PHP_EOL;
1080 $log_message = sprintf( "\tResize from: %sx%s to %sx%s", $current_size['width'], $current_size['height'], $new_width, $new_height ) . $nl;
1081 $log_message .= sprintf( "\tLarger resize from %sx%s", $resize_larger_w, $resize_larger_h ) . $nl;
1082 $log_message .= sprintf( "\tAbsolute path: %s", $this->path ) . $nl;
1083
1084 $resize_result = $image->resize( $new_width, $new_height, false );
1085
1086 if ( is_wp_error( $resize_result ) ) {
1087 $this->writeLog( sprintf( 'Resize error: %s. Details: %s', $resize_result->get_error_messages(), $log_message ) );
1088
1089 return false;
1090 }
1091
1092 $save_result = $image->save( $this->path );
1093
1094 if ( is_wp_error( $save_result ) ) {
1095 $this->writeLog( sprintf( 'Failed to save resized error in db: %s, Details: %s', $save_result->get_error_messages(), $log_message ) );
1096
1097 return false;
1098 }
1099
1100 $this->attachment_meta['width'] = $new_width;
1101 $this->attachment_meta['height'] = $new_height;
1102 $this->attachment_meta['old_width'] = $current_size['width'];
1103 $this->attachment_meta['old_height'] = $current_size['height'];
1104
1105 wp_update_attachment_metadata( $this->id, $this->attachment_meta );
1106
1107 return true;
1108 }
1109
1110 /**
1111 * Делает резервную копию
1112 *
1113 * @return true|WP_Error
1114 */
1115 protected function backup() {
1116 $backup = WIO_Backup::get_instance();
1117
1118 return $backup->backupAttachment( $this );
1119 }
1120
1121 /**
1122 * Восстанавливает файлы из резервной копии
1123 *
1124 * @return true|WP_Error
1125 */
1126 public function restore() {
1127 $backup = WIO_Backup::get_instance();
1128
1129 return $backup->restoreAttachment( $this );
1130 }
1131 }
1132