* The class to optimize image.
use WpOrg\Requests\Autoload;
use WpOrg\Requests\Requests;
defined('WPINC') || exit();
class Img_Optm extends Base {
const CLOUD_ACTION_NEW_REQ = 'new_req';
const CLOUD_ACTION_TAKEN = 'taken';
const CLOUD_ACTION_REQUEST_DESTROY = 'imgoptm_destroy';
const CLOUD_ACTION_CLEAN = 'clean';
const TYPE_NEW_REQ = 'new_req';
const TYPE_RESCAN = 'rescan';
const TYPE_DESTROY = 'destroy';
const TYPE_RESET_COUNTER = 'reset_counter';
const TYPE_CLEAN = 'clean';
const TYPE_PULL = 'pull';
const TYPE_BATCH_SWITCH_ORI = 'batch_switch_ori';
const TYPE_BATCH_SWITCH_OPTM = 'batch_switch_optm';
const TYPE_CALC_BKUP = 'calc_bkup';
const TYPE_RESET_ROW = 'reset_row';
const TYPE_RM_BKUP = 'rm_bkup';
const STATUS_NEW = 0; // 'new';
const STATUS_RAW = 1; // 'raw';
const STATUS_REQUESTED = 3; // 'requested';
const STATUS_NOTIFIED = 6; // 'notified';
const STATUS_DUPLICATED = 8; // 'duplicated';
const STATUS_PULLED = 9; // 'pulled';
const STATUS_FAILED = -1; // 'failed';
const STATUS_MISS = -3; // 'miss';
const STATUS_ERR_FETCH = -5; // 'err_fetch';
const STATUS_ERR_404 = -6; // 'err_404';
const STATUS_ERR_OPTM = -7; // 'err_optm';
const STATUS_XMETA = -8; // 'xmeta';
const STATUS_ERR = -9; // 'err';
const DB_SIZE = 'litespeed-optimize-size';
const DB_SET = 'litespeed-optimize-set';
const DB_NEED_PULL = 'need_pull';
private $_img_in_queue = [];
private $_existed_src_list = [];
private $_thumbnail_set = '';
private $_table_img_optm;
private $_table_img_optming;
private $_cron_ran = false;
private $_sizes_skipped = [];
public function __construct() {
Debug2::debug2('[ImgOptm] init');
$this->wp_upload_dir = wp_upload_dir();
$this->__media = $this->cls('Media');
$this->__data = $this->cls('Data');
$this->_table_img_optm = $this->__data->tb('img_optm');
$this->_table_img_optming = $this->__data->tb('img_optming');
$this->_summary = self::get_summary();
if (empty($this->_summary['next_post_id'])) {
$this->_summary['next_post_id'] = 0;
if ($this->conf(Base::O_IMG_OPTM_WEBP)) {
if ($this->conf(Base::O_IMG_OPTM_WEBP) == 2) {
// Allow users to ignore custom sizes.
$this->_sizes_skipped = apply_filters( 'litespeed_imgoptm_sizes_skipped', $this->conf( Base::O_IMG_OPTM_SIZES_SKIPPED ) );
* Gather images auto when update attachment meta
* This is to optimize new uploaded images first. Stored in img_optm table.
* Later normal process will auto remove these records when trying to optimize these images again
public function wp_update_attachment_metadata( $meta_value, $post_id ) {
self::debug2('🖌️ Auto update attachment meta [id] ' . $post_id);
if (empty($meta_value['file'])) {
if (!$this->_existed_src_list) {
// To aavoid extra query when recalling this function
self::debug('SELECT src from img_optm table');
if ($this->__data->tb_exist('img_optm')) {
$q = "SELECT src FROM `$this->_table_img_optm` WHERE post_id = %d";
$list = $wpdb->get_results($wpdb->prepare($q, $post_id));
$this->_existed_src_list[] = $post_id . '.' . $v->src;
if ($this->__data->tb_exist('img_optming')) {
$q = "SELECT src FROM `$this->_table_img_optming` WHERE post_id = %d";
$list = $wpdb->get_results($wpdb->prepare($q, $post_id));
$this->_existed_src_list[] = $post_id . '.' . $v->src;
$this->__data->tb_create('img_optming');
$this->tmp_pid = $post_id;
$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
$this->_append_img_queue($meta_value, true);
if (!empty($meta_value['sizes'])) {
foreach( $meta_value['sizes'] as $img_size_name => $img_size ){
$this->_append_img_queue($img_size, false, $img_size_name );
if (!$this->_img_in_queue) {
self::debug('auto update attachment meta 2 bypass: empty _img_in_queue');
// $this->_send_request();
public static function cron_auto_request() {
* Calculate wet run allowance
public function wet_limit() {
if (!empty($this->_summary['img_taken'])) {
$wet_limit = pow($this->_summary['img_taken'], 2);
if ($wet_limit == 1 && !empty($this->_summary['img_status.' . self::STATUS_ERR_OPTM])) {
$wet_limit = pow($this->_summary['img_status.' . self::STATUS_ERR_OPTM], 2);
if ($wet_limit < Cloud::IMG_OPTM_DEFAULT_GROUP) {
* Push raw img to image optm server
public function new_req() {
if (!empty($this->_summary['is_running']) && time() - $this->_summary['is_running'] < apply_filters('litespeed_imgoptm_new_req_interval', 3600)) {
self::debug('The previous req was in 3600s.');
$this->_summary['is_running'] = time();
// Check if has credit to push
$allowance = Cloud::cls()->allowance(Cloud::SVC_IMG_OPTM, $err);
$wet_limit = $this->wet_limit();
self::debug("allowance_max $allowance wet_limit $wet_limit");
if ($wet_limit && $wet_limit < $allowance) {
self::debug('❌ No credit');
Admin_Display::error(Error::msg($err));
$this->_finished_running();
self::debug('preparing images to push');
$this->__data->tb_create('img_optming');
$q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status = %d";
$q = $wpdb->prepare($q, array( self::STATUS_REQUESTED ));
$total_requested = $wpdb->get_var($q);
$max_requested = $allowance * 1;
if ($total_requested > $max_requested) {
self::debug('❌ Too many queued images (' . $total_requested . ' > ' . $max_requested . ')');
Admin_Display::error(Error::msg('too_many_requested'));
$this->_finished_running();
$allowance -= $total_requested;
self::debug('❌ Too many requested images ' . $total_requested);
Admin_Display::error(Error::msg('too_many_requested'));
$this->_finished_running();
// Limit maximum number of items waiting to be pulled
$q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status = %d";
$q = $wpdb->prepare($q, array( self::STATUS_NOTIFIED ));
$total_notified = $wpdb->get_var($q);
if ($total_notified > 0) {
self::debug('❌ Too many notified images (' . $total_notified . ')');
Admin_Display::error(Error::msg('too_many_notified'));
$this->_finished_running();
$q = "SELECT COUNT(1) FROM `$this->_table_img_optming` WHERE optm_status IN (%d, %d)";
$q = $wpdb->prepare($q, array( self::STATUS_NEW, self::STATUS_RAW ));
$total_new = $wpdb->get_var($q);
// $allowance -= $total_new;
// May need to get more images
$more = $allowance - $total_new;
$q = "SELECT b.post_id, b.meta_value
LEFT JOIN `$wpdb->postmeta` b ON b.post_id = a.ID
WHERE b.meta_key = '_wp_attachment_metadata'
AND a.post_type = 'attachment'
AND a.post_status = 'inherit'
AND a.post_mime_type IN ('image/jpeg', 'image/png', 'image/gif')
$q = $wpdb->prepare($q, array( $this->_summary['next_post_id'], $more ));
$list = $wpdb->get_results($q);
$this->_summary['next_post_id'] = $v->post_id;
$meta_value = $this->_parse_wp_meta_value($v);
$meta_value['file'] = wp_normalize_path($meta_value['file']);
$basedir = $this->wp_upload_dir['basedir'] . '/';
if (strpos($meta_value['file'], $basedir) === 0) {
$meta_value['file'] = substr($meta_value['file'], strlen($basedir));
$this->tmp_pid = $v->post_id;
$this->tmp_path = pathinfo($meta_value['file'], PATHINFO_DIRNAME) . '/';
$this->_append_img_queue($meta_value, true);
if (!empty($meta_value['sizes'])) {
foreach( $meta_value['sizes'] as $img_size_name => $img_size ){
$this->_append_img_queue($img_size, false, $img_size_name );
$num_a = count($this->_img_in_queue);
self::debug('Images found: ' . $num_a);
$this->_filter_duplicated_src();
self::debug('Images after duplicated: ' . count($this->_img_in_queue));
$this->_filter_invalid_src();
self::debug('Images after invalid: ' . count($this->_img_in_queue));
// Check w/ legacy imgoptm table, bypass finished images
$this->_filter_legacy_src();
$num_b = count($this->_img_in_queue);
self::debug('Images after filtered duplicated/invalid/legacy src: ' . $num_b);
$accepted_imgs = $this->_send_request($allowance);
$this->_finished_running();
$placeholder1 = Admin_Display::print_plural($accepted_imgs[0], 'image');
$placeholder2 = Admin_Display::print_plural($accepted_imgs[1], 'image');
$msg = sprintf(__('Pushed %1$s to Cloud server, accepted %2$s.', 'litespeed-cache'), $placeholder1, $placeholder2);
Admin_Display::success($msg);
private function _finished_running() {
$this->_summary['is_running'] = 0;
* Add a new img to queue which will be pushed to request
* @since 7.5 Allow to choose which image sizes should be optimized + added parameter $img_size_name.
private function _append_img_queue( $meta_value, $is_ori_file = false, $img_size_name = false ) {
if (empty($meta_value['file']) || empty($meta_value['width']) || empty($meta_value['height'])) {
self::debug2('bypass image due to lack of file/w/h: pid ' . $this->tmp_pid, $meta_value);
$short_file_path = $meta_value['file'];
// Test if need to skip image size.
$short_file_path = $this->tmp_path . $short_file_path;
$skip = false !== array_search( $img_size_name, $this->_sizes_skipped, true );
self::debug2( 'bypass image ' . $short_file_path . ' due to skipped size: ' . $img_size_name );
// Check if src is gathered already or not
if (in_array($this->tmp_pid . '.' . $short_file_path, $this->_existed_src_list)) {
// Debug2::debug2( '[Img_Optm] bypass image due to gathered: pid ' . $this->tmp_pid . ' ' . $short_file_path );
$this->_existed_src_list[] = $this->tmp_pid . '.' . $short_file_path;
// check file exists or not
$_img_info = $this->__media->info($short_file_path, $this->tmp_pid);
$extension = pathinfo($short_file_path, PATHINFO_EXTENSION);
if (!$_img_info || !in_array($extension, array( 'jpg', 'jpeg', 'png', 'gif' ))) {
self::debug2('bypass image due to file not exist: pid ' . $this->tmp_pid . ' ' . $short_file_path);
// Check if optimized file exists or not
$target_file_path = $short_file_path . '.' . $this->_format;
if (!$this->__media->info($target_file_path, $this->tmp_pid)) {
if ($this->conf(self::O_IMG_OPTM_ORI)) {
$target_file_path = substr($short_file_path, 0, -strlen($extension)) . 'bk.' . $extension;
if (!$this->__media->info($target_file_path, $this->tmp_pid)) {
self::debug2('bypass image due to optimized file exists: pid ' . $this->tmp_pid . ' ' . $short_file_path);
// Debug2::debug2( '[Img_Optm] adding image: pid ' . $this->tmp_pid );
$this->_img_in_queue[] = array(
'md5' => $_img_info['md5'],
'url' => $_img_info['url'],
'src' => $short_file_path, // not needed in LiteSpeed IAPI, just leave for local storage after post
'mime_type' => !empty($meta_value['mime-type']) ? $meta_value['mime-type'] : '',
* Save gathered image raw data
private function _save_raw() {
if (empty($this->_img_in_queue)) {
foreach ($this->_img_in_queue as $k => $v) {
$_img_info = $this->__media->info($v['src'], $v['pid']);
// attachment doesn't exist, delete the record
if (empty($_img_info['url']) || empty($_img_info['md5'])) {
unset($this->_img_in_queue[$k]);
$pid_list[] = (int) $v['pid'];
$data[] = self::STATUS_RAW;
$fields = 'post_id, optm_status, src';
$q = "INSERT INTO `$this->_table_img_optming` ( $fields ) VALUES ";
$q .= Utility::chunk_placeholder($data, $fields);
$wpdb->query($wpdb->prepare($q, $data));
$count = count($this->_img_in_queue);
self::debug('Added raw images [total] ' . $count);
$this->_img_in_queue = [];
// Save thumbnail groups for future rescan index
$this->_gen_thumbnail_set();
$pid_list = array_unique($pid_list);
self::debug('pid list to append to postmeta', $pid_list);
$pid_list = array_diff($pid_list, $this->_pids_set);
$this->_pids_set = array_merge($this->_pids_set, $pid_list);
$existed_meta = $wpdb->get_results("SELECT * FROM `$wpdb->postmeta` WHERE post_id IN ('" . implode("','", $pid_list) . "') AND meta_key='" . self::DB_SET . "'");
foreach ($existed_meta as $v) {
$existed_pid[] = $v->post_id;
self::debug('pid list to update postmeta', $existed_pid);
$wpdb->prepare("UPDATE `$wpdb->postmeta` SET meta_value=%s WHERE post_id IN ('" . implode("','", $existed_pid) . "') AND meta_key=%s", array(
$new_pids = $existed_pid ? array_diff($pid_list, $existed_pid) : $pid_list;