File: /var/www/delta/wp-content/plugins/sg-cachepress/core/Images_Optimizer/Images_Optimizer.php
<?php
namespace SiteGround_Optimizer\Images_Optimizer;
use SiteGround_Optimizer\Supercacher\Supercacher;
use SiteGround_Optimizer\Options\Options;
use SiteGround_Optimizer\Front_End_Optimization\Front_End_Optimization;
use SiteGround_Helper\Helper_Service;
/**
* SG Images_Optimizer main plugin class
*/
class Images_Optimizer extends Abstract_Images_Optimizer {
/**
* Set max image width. Default is 2560.
*
* @since 6.0.0
*
* @var int max width value.
*/
const MAX_IMAGE_WIDTH = 2560;
/**
* Array containing options used for status updates.
*
* @var array
*/
public $options_map = array(
'completed' => 'siteground_optimizer_image_optimization_completed',
'status' => 'siteground_optimizer_image_optimization_status',
'stopped' => 'siteground_optimizer_image_optimization_stopped',
);
/**
* The type of image optimization.
*
* @var string
*/
public $type = 'image';
/**
* The total non-optimized images option.
*
* @var string
*/
public $non_optimized = 'siteground_optimizer_total_unoptimized_images';
/**
* The batch name
*
* @var string
*/
public $batch_skipped = 'siteground_optimizer_is_optimized';
/**
* The ajax action we are using.
*
* @var string
*/
public $action = 'siteground_optimizer_start_image_optimization';
/**
* Array containing all process
*
* @var array
*/
public $process_map = array(
'filter' => 'siteground_optimizer_image_optimization_timeout',
'attempts' => 'siteground_optimizer_optimization_attempts',
'failed' => 'siteground_optimizer_optimization_failed',
);
/**
* The type of cron we want to fire.
*
* @var string
*/
public $cron_type = 'siteground_optimizer_start_image_optimization_cron';
/**
* The process lock we are using.
*
* @var string
*/
public $process_lock = 'siteground_optimizer_image_optimization_lock';
/**
* The map for the compression levels for different file types.
*
* @var array
*/
public $compression_level_map = array(
// IMAGETYPE_GIF.
1 => array(
'1' => '-O1', // Low.
'2' => '-O2', // Medium.
'3' => '-O3', // High.
),
// IMAGETYPE_JPEG.
2 => array(
'1' => '-m85', // Low.
'2' => '-m60', // Medium.
'3' => '-m20', // High.
),
// IMAGETYPE_PNG.
3 => array(
'1' => '-o1',
'2' => '-o2',
'3' => '-o3',
),
);
/**
* The map for the maximum image width option.
*
* @var array
*/
public $default_max_width_sizes = array(
array(
'label' => '2560px',
'value' => 2560,
'selected' => 0,
),
array(
'label' => '2048px',
'value' => 2048,
'selected' => 0,
),
array(
'label' => '1920px',
'value' => 1920,
'selected' => 0,
),
array(
'label' => 'Disabled',
'value' => 0,
'selected' => 0,
),
);
/**
* Optimize the image
*
* @since 5.0.0
*
* @param int $id The image id.
* @param array $metadata The image metadata.
*
* @return bool True on success, false on failure.
*/
public function optimize( $id, $metadata ) {
// Load the uploads dir.
$upload_dir = wp_get_upload_dir();
// Get path to main image.
$main_image = get_attached_file( $id );
// Bail if the override is disabled and the image has a custom compression level.
if (
1 !== intval( get_option( 'siteground_optimizer_overwrite_custom' ) ) &&
! empty( get_post_meta( $id, 'siteground_optimizer_compression_level', true ) )
) {
return false;
}
// Get the basename.
$basename = basename( $main_image );
// Get the command placeholder. It will be used by main image and to optimize the different image sizes.
$status = $this->execute_optimization_command( $main_image );
// Optimization failed.
if ( true === boolval( $status ) ) {
update_post_meta( $id, 'siteground_optimizer_optimization_failed', 1 );
return false;
}
// Check if there are any sizes.
if ( ! empty( $metadata['sizes'] ) ) {
// Loop through all image sizes and optimize them as well.
foreach ( $metadata['sizes'] as $size ) {
// Replace main image with the cropped image and run the optimization command.
$status = $this->execute_optimization_command( str_replace( $basename, $size['file'], $main_image ) );
// Optimization failed.
if ( true === boolval( $status ) ) {
update_post_meta( $id, 'siteground_optimizer_optimization_failed', 1 );
return false;
}
}
}
// Save the original filesize in new post meta.
update_post_meta( $id, 'siteground_optimizer_original_filesize', $metadata['filesize'] ) ;
// Replace the filesize in the metadata.
$metadata['filesize'] = filesize( $main_image );
// Update the attachment metadata.
wp_update_attachment_metadata( $id, $metadata );
// Everything ran smoothly.
update_post_meta( $id, 'siteground_optimizer_is_optimized', 1 );
return true;
}
/**
* Resize the uploaded image if width is greather than allowed.
*
* @since 6.0.0
*
* @param array $image_data - contains file, url, type.
*/
public function resize( $image_data ) {
// Getting the option value from the db and applying additional filters, if any.
$image_resize_option = apply_filters( 'sgo_set_max_image_width', get_option( 'siteground_optimizer_resize_images', 2560 ) );
// Disable resize, if it's set so in the DB and no filters are found.
if ( 0 === intval ( $image_resize_option ) ) {
return false;
}
// Adding a min value.
$image_resize_option = intval( $image_resize_option ) < 1200 ? 1200 : intval( $image_resize_option );
return intval( $image_resize_option );
}
/**
* Check if image exists and perform optimiation.
*
* @since 5.0.0
*
* @param string $filepath The path to the file.
*
* @return bool False on success, true on failure.
*/
private function execute_optimization_command( $filepath, $compression_level = null ) {
// Bail if the file doens't exists.
if ( ! file_exists( $filepath ) ) {
return true;
}
// Get option for the selected compression level.
$compression_level = is_null( $compression_level ) ? intval( get_option( 'siteground_optimizer_compression_level' ) ) : $compression_level;
// Bail if compression level is set to None.
if ( 0 === $compression_level ) {
return true;
}
$backup_filepath = preg_replace( '~.(png|jpg|jpeg|gif)$~', '.bak.$1', $filepath );
if (
Options::is_enabled( 'siteground_optimizer_backup_media' ) &&
! file_exists( $backup_filepath )
) {
copy( $filepath, $backup_filepath );
}
$status = $this->optimize_image(
file_exists( $backup_filepath ) ? $backup_filepath : $filepath,
$compression_level
);
// Create webp copy of the webp is enabled.
if ( Options::is_enabled( 'siteground_optimizer_webp_support' ) ) {
Images_Optimizer_Webp::generate_webp_file( $filepath );
}
return $status;
}
/**
* Optimize the image.
*
* @since 6.0.0
*
* @param string $filepath The image filepath.
* @param int $level The optimization level.
*
* @return string The status code.
*/
public function optimize_image( $filepath, $level ) {
// Get image type.
$type = exif_imagetype( $filepath );
$output_filepath = preg_replace( '~\.bak.(png|jpg|jpeg|gif)$~', '.$1', $filepath );
switch ( $type ) {
case IMAGETYPE_GIF:
$placeholder = 'gifsicle %s --careful %s -o %s 2>&1';
break;
case IMAGETYPE_JPEG:
// DO NOT REMOVE THE LINE BELOW!
// The jpegoptim doesn't support input/output params, so we need to create a backup of the original image.
// However, if the filepaths are the same, this is skipped.
if ( $filepath !== $output_filepath ) {
copy( $filepath, $output_filepath );
}
$placeholder = 'jpegoptim %1$s %3$s 2>&1';
break;
case IMAGETYPE_PNG:
// Bail if the image is bigger than 500k.
// PNG usage is not recommended and images bigger than 500kb
// hit the limits.
if ( filesize( $filepath ) > self::PNGS_SIZE_LIMIT ) {
return true;
}
$placeholder = 'optipng %s %s -out=%s 2>&1';
break;
default:
// Bail if the image type is not supported.
return true;
}
// Optimize the image.
exec(
sprintf(
$placeholder, // The command.
$this->compression_level_map[ $type ][ $level ], // The compression level.
$filepath, // Image path.
$output_filepath // New Image path.
),
$output,
$status
);
return $status;
}
/**
* Optimize the preview image.
*
* @since 6.0.0
*
* @param int $id The attachment id.
*/
public function get_preview_images( $id ) {
$filepath = ! empty( $id ) ? get_attached_file( $id ) : \SiteGround_Optimizer\DIR . '/assets/images/preview.jpg';
$urls = array(
0 => array(
'compression' => 0,
'url' => str_replace( ABSPATH, Helper_Service::get_home_url(), $filepath ),
'size' => $this->get_human_readable_size( $filepath ),
),
);
$output_dir = Front_End_Optimization::get_instance()->get_assets_dir() . 'previews/';
$basename = basename( $filepath );
if ( ! is_dir( $output_dir ) ) {
Front_End_Optimization::get_instance()->create_directory( $output_dir );
}
foreach ( $this->compression_level_map as $type => $levels ) {
// Generate output folder.
$output = $output_dir . $basename;
// Copy the image.
copy( $filepath, $output );
$this->optimize_image( $output, $type );
$new_filename = str_replace( basename( $output ), $type . '-' . $basename, $output );
rename( // phpcs:ignore
$output,
$new_filename
);
$urls[ $type ]['url'] = str_replace( ABSPATH, Helper_Service::get_home_url(), $new_filename );
$urls[ $type ]['compression'] = intval( $type );
$urls[ $type ]['size'] = $this->get_human_readable_size( $new_filename );
}
return $urls;
}
/**
* Get a human readable string containing information regarding the size of a file.
*
* @since 6.0.0
*
* @param string $filepath The path of the file that has to be sized.
*
* @return string The human-readable string with the size of the file.
*/
public function get_human_readable_size( $filepath ) {
// Get the size of the file in bytes.
$size = filesize( $filepath );
// Possible unit types.
$units = array( 'B', 'kB', 'MB' );
$step = 1024;
$i = 0;
// Divide the size until it's less than 1 to find the correct unit.
while ( ( $size / $step ) > 0.9 ) {
$size = $size / $step;
$i++;
}
// Return the human readable string.
return round( $size, 2 ) . $units[ $i ];
}
/**
* Restore the original images.
*
* @since 6.0.0
*
* @return The result of restore.
*/
public function restore_originals() {
$basedir = Helper_Service::get_uploads_dir();
exec( "find $basedir -regextype posix-extended -type f -regex '.*bak.(png|jpg|jpeg|gif)$' -exec rename '.bak' '' {} \;", $output, $result );
// Reset the images metadata.
$this->reset_images_filesize_meta();
// Reset the optimization status.
$this->reset_image_optimization_status();
return $result;
}
/**
* Delete the backup image on image delete.
*
* @since 6.0.0
*
* @param int $id The attachment ID.
*/
public function delete_backups( $id ) {
global $wp_filesystem;
$main_image = get_attached_file( $id );
$metadata = wp_get_attachment_metadata( $id );
$basename = basename( $main_image );
$wp_filesystem->delete( preg_replace( '~.(png|jpg|jpeg|gif)$~', '.bak.$1', $main_image ) );
if ( ! empty( $metadata['sizes'] ) ) {
// Loop through all image sizes and optimize them as well.
foreach ( $metadata['sizes'] as $size ) {
$wp_filesystem->delete( preg_replace( '~.(png|jpg|jpeg|gif)$~', '.bak.$1', str_replace( $basename, $size['file'], $main_image ) ) );
}
}
}
/**
* Add custom metabox for compression level per attachment, on the media screen.
*
* @since 6.0.6
*
* @param array $form_fields Fields of the edit_attachment form.
* @param WP_Post $post The object containing the attachment.
*
* @return array Fields of the edit_attachment form.
*/
public function custom_attachment_compression_level_field( $form_fields, $post ) {
// Get current attachment compression level.
$field_value = get_post_meta( $post->ID, 'siteground_optimizer_compression_level', true );
// If field value is empty - fallback to site global option.
if ( ! is_numeric( $field_value ) ) {
$field_value = get_option( 'siteground_optimizer_compression_level' );
}
// The field html.
$html = '<select name="compression_level">';
// Select options.
$options = array(
'None',
'Low',
'Medium',
'High',
);
// Add the select options to the html.
foreach ( $options as $key => $value ) {
$html .= '<option' . selected( $field_value, $key, false ) . ' value="' . $key . '">' . $value . '</option>';
}
$html .= '</select>';
$form_fields['compression_level'] = array(
'value' => $field_value ? intval( $field_value ) : '',
'label' => __( 'Compression Level', 'sg-cachepress' ),
'input' => 'html',
'html' => $html,
);
return $form_fields;
}
/**
* Saving the new meta for the compression level of the attachment.
*
* @since 6.0.6
*
* @param int $attachment_id ID of the attachment.
*
* @return bool|string Status code of the compression.
*/
public function custom_attachment_compression_level( $attachment_id ) {
if ( ! isset( $_REQUEST['compression_level'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return $attachment_id;
}
// Update the attachment's meta.
update_post_meta( $attachment_id, 'siteground_optimizer_compression_level', $_REQUEST['compression_level'] ); // phpcs:ignore
// Get attachment's filepath.
$filepath = get_attached_file( $attachment_id );
// Find backup image path.
$backup_filepath = preg_replace( '~.(png|jpg|jpeg|gif)$~', '.bak.$1', $filepath );
// Check if backup file exists, if so, replace the file with the original one.
if ( file_exists( $backup_filepath ) ) {
copy( $backup_filepath, $filepath );
}
// Compress the image only if the compression level is different than none.
if ( 0 !== intval( $_REQUEST['compression_level'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// Optimize the image with the new compression level.
return $this->execute_optimization_command( $filepath, intval( $_REQUEST['compression_level'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
}
/**
* Retrieves default sizes and sets the selected value.
*
* @since 7.1.1
*
* @return array Array with the labels, values and selected properties.
*/
public function prepare_max_width_sizes() {
// Get set option for the resize, using 2560 for default.
$image_resize_option = (int) get_option( 'siteground_optimizer_resize_images', 2560 );
// Retrieve local array.
$sizes = $this->default_max_width_sizes;
// Iterate and set the correct selected value.
foreach ( $sizes as $index => $size ) {
if ( $image_resize_option === $sizes[ $index ]['value'] ) {
$sizes[ $index ]['selected'] = 1;
}
}
// Returns the modified array.
return $sizes;
}
/**
* Reset the restored images original filesize in the metadata.
*
* @since 7.3.2
*/
public function reset_images_filesize_meta() {
// Get all images with backup filesize metadata available.
$images = get_posts(
array(
'post_type' => 'attachment',
'post_mime_type' => 'image',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array(
array(
'key' => 'siteground_optimizer_original_filesize',
'compare' => 'EXISTS',
),
),
)
);
// Bail if we have no images with backup filesize metadata.
if ( empty( $images ) ) {
return;
}
// Restore the filesize metadata.
foreach( $images as $image_id ) {
// Get the image metadata.
$metadata = wp_get_attachment_metadata( $image_id );
// Restore the original filesize metdata.
$metadata['filesize'] = get_post_meta( $image_id, 'siteground_optimizer_original_filesize', true );
// Update the attachment metadata.
wp_update_attachment_metadata( $image_id, $metadata );
}
}
}