* The class to operate media data.
* @since 1.5 Moved into /inc
* @author LiteSpeed Technologies <info@litespeedtech.com>
defined('WPINC') || exit();
const LIB_FILE_IMG_LAZYLOAD = 'assets/js/lazyload.min.js';
private $_vpi_preload_list = array();
public function __construct()
Debug2::debug2('[Media] init');
$this->_wp_upload_dir = wp_upload_dir();
// Due to ajax call doesn't send correct accept header, have to limit webp to HTML only
if (defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_IMG_OPTM_WEBP)) {
if ($this->webp_support()) {
if (function_exists('wp_calculate_image_srcset')) {
add_filter('wp_calculate_image_srcset', array($this, 'webp_srcset'), 988);
// add_filter( 'wp_get_attachment_image_src', array( $this, 'webp_attach_img_src' ), 988 );// todo: need to check why not
// add_filter( 'wp_get_attachment_url', array( $this, 'webp_url' ), 988 ); // disabled to avoid wp-admin display
if ($this->conf(Base::O_MEDIA_LAZY) && !$this->cls('Metabox')->setting('litespeed_no_image_lazy')) {
self::debug('Suppress default WP lazyload');
add_filter('wp_lazy_loading_enabled', '__return_false');
add_filter('litespeed_buffer_finalize', array($this, 'finalize'), 4);
add_filter('litespeed_optm_html_head', array($this, 'finalize_head'));
* Add featured image to head
public function finalize_head($content)
// <link rel="preload" as="image" href="xx">
if ($this->_vpi_preload_list) {
foreach ($this->_vpi_preload_list as $v) {
$content .= '<link rel="preload" as="image" href="' . $v . '">';
// $featured_image_url = get_the_post_thumbnail_url();
// if ($featured_image_url) {
// self::debug('Append featured image to head: ' . $featured_image_url);
// if ((defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_IMG_OPTM_WEBP)) && $this->webp_support()) {
// $featured_image_url = $this->replace_webp($featured_image_url) ?: $featured_image_url;
* Adjust WP default JPG quality
public function adjust_jpg_quality($quality)
$v = $this->conf(Base::O_IMG_OPTM_JPG_QUALITY);
public function after_admin_init()
add_filter('jpeg_quality', array($this, 'adjust_jpg_quality'));
add_filter('manage_media_columns', array($this, 'media_row_title'));
add_filter('manage_media_custom_column', array($this, 'media_row_actions'), 10, 2);
add_action('litespeed_media_row', array($this, 'media_row_con'));
// Hook to attachment delete action
add_action('delete_attachment', __CLASS__ . '::delete_attachment');
* Media delete action hook
public static function delete_attachment($post_id)
// if (!Data::cls()->tb_exist('img_optm')) {
self::debug('delete_attachment [pid] ' . $post_id);
Img_Optm::cls()->reset_row($post_id);
* Return media file info if exists
* This is for remote attachment plugins
public function info($short_file_path, $post_id)
$short_file_path = wp_normalize_path($short_file_path);
$basedir = $this->_wp_upload_dir['basedir'] . '/';
if (strpos($short_file_path, $basedir) === 0) {
$short_file_path = substr($short_file_path, strlen($basedir));
$real_file = $basedir . $short_file_path;
if (file_exists($real_file)) {
'url' => $this->_wp_upload_dir['baseurl'] . '/' . $short_file_path,
'md5' => md5_file($real_file),
'size' => filesize($real_file),
* WP Stateless compatibility #143 https://github.com/litespeedtech/lscache_wp/issues/143
* @return array( 'url', 'md5', 'size' )
$info = apply_filters('litespeed_media_info', array(), $short_file_path, $post_id);
if (!empty($info['url']) && !empty($info['md5']) && !empty($info['size'])) {
public function del($short_file_path, $post_id)
$real_file = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path;
if (file_exists($real_file)) {
self::debug('deleted ' . $real_file);
do_action('litespeed_media_del', $short_file_path, $post_id);
public function rename($short_file_path, $short_file_path_new, $post_id)
// self::debug('renaming ' . $short_file_path . ' -> ' . $short_file_path_new);
$real_file = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path;
$real_file_new = $this->_wp_upload_dir['basedir'] . '/' . $short_file_path_new;
if (file_exists($real_file)) {
rename($real_file, $real_file_new);
self::debug('renamed ' . $real_file . ' to ' . $real_file_new);
do_action('litespeed_media_rename', $short_file_path, $short_file_path_new, $post_id);
* Media Admin Menu -> Image Optimization Column Title
public function media_row_title($posts_columns)
$posts_columns['imgoptm'] = __('LiteSpeed Optimization', 'litespeed-cache');
* Media Admin Menu -> Image Optimization Column
public function media_row_actions($column_name, $post_id)
if ($column_name !== 'imgoptm') {
do_action('litespeed_media_row', $post_id);
* Display image optm info
public function media_row_con($post_id)
$att_info = wp_get_attachment_metadata($post_id);
if (empty($att_info['file'])) {
$short_path = $att_info['file'];
$size_meta = get_post_meta($post_id, Img_Optm::DB_SIZE, true);
if ($size_meta && !empty($size_meta['ori_saved'])) {
$percent = ceil(($size_meta['ori_saved'] * 100) / $size_meta['ori_total']);
$extension = pathinfo($short_path, PATHINFO_EXTENSION);
$bk_file = substr($short_path, 0, -strlen($extension)) . 'bk.' . $extension;
$bk_optm_file = substr($short_path, 0, -strlen($extension)) . 'bk.optm.' . $extension;
$link = Utility::build_url(Router::ACTION_IMG_OPTM, 'orig' . $post_id);
if ($this->info($bk_file, $post_id)) {
$curr_status = __('(optm)', 'litespeed-cache');
$desc = __('Currently using optimized version of file.', 'litespeed-cache') . ' ' . __('Click to switch to original (unoptimized) version.', 'litespeed-cache');
} elseif ($this->info($bk_optm_file, $post_id)) {
$cls .= ' litespeed-warning';
$curr_status = __('(non-optm)', 'litespeed-cache');
$desc = __('Currently using original (unoptimized) version of file.', 'litespeed-cache') . ' ' . __('Click to switch to optimized version.', 'litespeed-cache');
sprintf(__('Original file reduced by %1$s (%2$s)', 'litespeed-cache'), $percent . '%', Utility::real_size($size_meta['ori_saved'])),
echo sprintf(__('Orig saved %s', 'litespeed-cache'), $percent . '%');
' <a href="%1$s" class="litespeed-media-href %2$s" data-balloon-pos="left" data-balloon-break aria-label="%3$s">%4$s</a>',
' <span class="litespeed-desc" data-balloon-pos="left" data-balloon-break aria-label="%1$s">%2$s</span>',
__('Using optimized version of file. ', 'litespeed-cache') . ' ' . __('No backup of original file exists.', 'litespeed-cache'),
__('(optm)', 'litespeed-cache')
} elseif ($size_meta && $size_meta['ori_saved'] === 0) {
echo GUI::pie_tiny(0, 24, __('Congratulation! Your file was already optimized', 'litespeed-cache'), 'left');
echo sprintf(__('Orig %s', 'litespeed-cache'), '<span class="litespeed-desc">' . __('(no savings)', 'litespeed-cache') . '</span>');
echo __('Orig', 'litespeed-cache') . '<span class="litespeed-left10">—</span>';
if ($size_meta && !empty($size_meta['webp_saved'])) {
$percent = ceil(($size_meta['webp_saved'] * 100) / $size_meta['webp_total']);
$link = Utility::build_url(Router::ACTION_IMG_OPTM, 'webp' . $post_id);
if ($this->info($short_path . '.webp', $post_id)) {
$curr_status = __('(optm)', 'litespeed-cache');
__('Currently using optimized version of WebP file.', 'litespeed-cache') .
__('Click to switch to original (unoptimized) version.', 'litespeed-cache');
} elseif ($this->info($short_path . '.optm.webp', $post_id)) {
$cls .= ' litespeed-warning';
$curr_status = __('(non-optm)', 'litespeed-cache');
__('Currently using original (unoptimized) version of WebP file.', 'litespeed-cache') .
__('Click to switch to optimized version.', 'litespeed-cache');
sprintf(__('WebP file reduced by %1$s (%2$s)', 'litespeed-cache'), $percent . '%', Utility::real_size($size_meta['webp_saved'])),
echo sprintf(__('WebP saved %s', 'litespeed-cache'), $percent . '%');
' <a href="%1$s" class="litespeed-media-href %2$s" data-balloon-pos="left" data-balloon-break aria-label="%3$s">%4$s</a>',
' <span class="litespeed-desc" data-balloon-pos="left" data-balloon-break aria-label="%1$s">%2$s</span>',
__('Using optimized version of file. ', 'litespeed-cache') . ' ' . __('No backup of unoptimized WebP file exists.', 'litespeed-cache'),
__('(optm)', 'litespeed-cache')
echo __('WebP', 'litespeed-cache') . '<span class="litespeed-left10">—</span>';
'<div class="row-actions"><span class="delete"><a href="%1$s" class="">%2$s</a></span></div>',
Utility::build_url(Router::ACTION_IMG_OPTM, Img_Optm::TYPE_RESET_ROW, false, null, array('id' => $post_id)),
__('Restore from backup', 'litespeed-cache')
* NOTE: this is not used because it has to be after admin_init
* @return array $sizes Data for all currently-registered image sizes.
public function get_image_sizes()
global $_wp_additional_image_sizes;
foreach (get_intermediate_image_sizes() as $_size) {
if (in_array($_size, array('thumbnail', 'medium', 'medium_large', 'large'))) {
$sizes[$_size]['width'] = get_option($_size . '_size_w');
$sizes[$_size]['height'] = get_option($_size . '_size_h');
$sizes[$_size]['crop'] = (bool) get_option($_size . '_crop');
} elseif (isset($_wp_additional_image_sizes[$_size])) {
'width' => $_wp_additional_image_sizes[$_size]['width'],
'height' => $_wp_additional_image_sizes[$_size]['height'],
'crop' => $_wp_additional_image_sizes[$_size]['crop'],
* Exclude role from optimization filter
public function webp_support()
if (!empty($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'image/webp') !== false) {
if (!empty($_SERVER['HTTP_USER_AGENT'])) {
$user_agents = array('chrome-lighthouse', 'googlebot', 'page speed');
foreach ($user_agents as $user_agent) {
if (stripos($_SERVER['HTTP_USER_AGENT'], $user_agent) !== false) {
if (preg_match('/iPhone OS (\d+)_/i', $_SERVER['HTTP_USER_AGENT'], $matches)) {
$lscwp_ios_version = $matches[1];
if ($lscwp_ios_version >= 14) {
* NOTE: As this is after cache finalized, can NOT set any cache control anymore
* Only do for main page. Do NOT do for esi or dynamic content.
* @return string The buffer
public function finalize($content)
if (defined('LITESPEED_NO_LAZY')) {
Debug2::debug2('[Media] bypass: NO_LAZY const');
if (!defined('LITESPEED_IS_HTML')) {
Debug2::debug2('[Media] bypass: Not frontend HTML type');
if (!Control::is_cacheable()) {
self::debug('bypass: Not cacheable');
$this->content = $content;
* Run lazyload replacement for images in buffer