: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* PNG to JPG conversion: Png2jpg class
* @package Smush\Core\Modules
* @author Umesh Kumar <umesh@incsub.com>
* @copyright (c) 2016, Incsub (http://incsub.com)
namespace Smush\Core\Modules;
if ( ! defined( 'WPINC' ) ) {
class Png2jpg extends Abstract_Module {
protected $slug = 'png_to_jpg';
* Whether module is pro or not.
protected $is_pro = true;
* Check if Imagick is available or not
* @return bool True/False Whether Imagick is available or not
private function supports_imagick() {
if ( ! class_exists( '\Imagick' ) ) {
* @return bool True/False Whether GD is available or not
private function supports_gd() {
if ( ! function_exists( 'gd_info' ) ) {
* Save can be converted status before resizing the image,
* because the the image might be lost the undefined-transparent behavior after resizing.
* @see WP_Image_Editor_Imagick::thumbnail_image()
* WP Resize function will convert Imagick::ALPHACHANNEL_UNDEFINED -> Imagick::ALPHACHANNEL_OPAQUE.
* @param array $sizes Array of sizes containing max width and height for all the uploaded images.
* @param string $file_path Image file path.
* @param int $id Image id.
* @return array the Original $sizes.
public function cache_can_be_converted_status( $sizes, $file_path, $id ) {
// Call can_be_converted and cache the status.
$this->can_be_converted( $id, 'full', '', $file_path );
* Checks if the Given PNG file is transparent or not
* @param string $id Attachment ID.
* @param string $file File path for the attachment.
private function is_transparent( $id = '', $file = '' ) {
// No attachment id/ file path, return.
if ( empty( $id ) && empty( $file ) ) {
// This downloads the file from S3 when S3 is enabled.
$file = Helper::get_attached_file( $id );
if ( empty( $file ) || ! file_exists( $file ) ) {
Helper::logger()->png2jpg()->info( sprintf( 'File [%s(%d)] is empty or does not exist.', Helper::clean_file_path( $file ), $id ) );
// Try to get transparency using Imagick.
if ( $this->supports_imagick() ) {
$im = new Imagick( $file );
return $im->getImageAlphaChannel();
} catch ( Exception $e ) {
Helper::logger()->png2jpg()->error( 'Imagick: Error in checking PNG transparency ' . $e->getMessage() );
// Src: http://camendesign.com/code/uth1_is-png-32bit.
if ( ord( file_get_contents( $file, false, null, 25, 1 ) ) & 4 ) {
Helper::logger()->png2jpg()->info( sprintf( 'File [%s(%d)] is an PNG 32-bit.', Helper::clean_file_path( $file ), $id ) );
// Src: http://www.jonefox.com/blog/2011/04/15/how-to-detect-transparency-in-png-images/.
$contents = file_get_contents( $file );
if ( stripos( $contents, 'PLTE' ) !== false && stripos( $contents, 'tRNS' ) !== false ) {
Helper::logger()->png2jpg()->info( sprintf( 'File [%s(%d)] is an PNG 8-bit.', Helper::clean_file_path( $file ), $id ) );
// If both the conditions failed, that means not transparent.
// If Imagick is installed, and the code exited due to some error.
if ( empty( $transparent ) && $this->supports_gd() ) {
// Check for transparency using GD.
$i = imagecreatefrompng( $file );
$palette = ( imagecolortransparent( $i ) < 0 );
* Check if given attachment id can be converted to JPEG or not
* @param string $id Atachment ID.
* @param string $size Image size.
* @param string $mime Mime type.
* @param string $file File.
* @since 3.9.6 We removed the private method should_convert
* and we also handled the case which we need to delete the download file inside S3.
* Note, we also used this for checking resmush, so we only download the attached file (s3)
* if it's necessary (self::is_transparent()). Check the comment on self::__construct() for detail.
* @return bool True/False Can be converted or not
public function can_be_converted( $id = '', $size = 'full', $mime = '', $file = '' ) {
// If PNG2JPG not enabled, or is not smushable, return.
if ( ! $this->is_active() || ! Helper::is_smushable( $id ) ) {
// Check it from the cache for full size.
if ( 'full' === $size && null !== Helper::cache_get( $id, 'png2jpg_can_be_converted' ) ) {
return Helper::cache_get( $id, 'png2jpg_can_be_converted' );
$mime = empty( $mime ) ? get_post_mime_type( $id ) : $mime;
if ( 'image/png' !== $mime && 'image/x-png' !== $mime ) {
// Return if Imagick and GD is not available.
if ( ! $this->supports_imagick() && ! $this->supports_gd() ) {
Helper::logger()->png2jpg()->warning( 'The site does not support Imagick or GD.' );
// If already tried the conversion.
if ( get_post_meta( $id, 'wp-smush-pngjpg_savings', true ) ) {
Helper::logger()->png2jpg()->info( sprintf( 'File [%s(%d)] already tried the conversion.', Helper::clean_file_path( $file ), $id ) );
// Check if registered size is supposed to be converted or not.
if ( 'full' !== $size && WP_Smush::get_instance()->core()->mod->smush->skip_image_size( $size ) ) {
// Make sure $file is not empty.
$file = Helper::get_attached_file( $id );// S3+.
* Filter whether to convert the PNG to JPG or not
* @param bool $should_convert Current choice for image conversion
* @param int $id Attachment id
* @param string $file File path for the image
* @param string $size Image size being converted
$should_convert = apply_filters( 'wp_smush_convert_to_jpg', ! $this->is_transparent( $id, $file ), $id, $file, $size );
if ( 'full' === $size ) {
* We used this method inside Backup::create_backup(), Smush function, and filter wp_smush_resize_sizes,
* so cache the result to avoid to check it again.
Helper::cache_set( $id, $should_convert, 'png2jpg_can_be_converted' );
* Check whether to resmush image or not.
* @usedby Smush\App\Ajax::scan_images()
* @param bool $should_resmush Current status.
* @param int $attachment_id Attachment ID.
* @return bool|string png2jpg|TRUE|FALSE
public function should_resmush( $should_resmush, $attachment_id ) {
if ( ! $should_resmush && $this->can_be_converted( $attachment_id ) ) {
$should_resmush = 'png2jpg';
* Update the image URL, MIME Type, Attached File, file path in Meta, URL in post content
* @param string $id Attachment ID.
* @param string $o_file Original File Path that has to be replaced.
* @param string $n_file New File Path which replaces the old file.
* @param string $meta Attachment Meta.
* @param string $size_k Image Size.
* @param string $o_type Operation Type "conversion", "restore".
* @return array Attachment Meta with updated file path.
public function update_image_path( $id, $o_file, $n_file, $meta, $size_k, $o_type = 'conversion' ) {
$upload_dir = wp_upload_dir();
$upload_path = trailingslashit( $upload_dir['basedir'] );
$dir_name = pathinfo( $o_file, PATHINFO_DIRNAME );
// Full Path to new file.
$n_file_path = path_join( $dir_name, $n_file );
// Current URL for image.
$o_url = wp_get_attachment_url( $id );
// Update URL for image size.
if ( 'full' !== $size_k ) {
$base_url = dirname( $o_url );
$o_url = $base_url . '/' . basename( $o_file );
// Update File path, Attached File, GUID.
$meta = empty( $meta ) ? wp_get_attachment_metadata( $id ) : $meta;
$mime = Helper::get_mime_type( $n_file_path );
* If there's no fileinfo extension installed, the mime type will be returned as false.
* As a fallback, we set it manually.
$mime = 'conversion' === $o_type ? 'image/jpeg' : 'image/png';
// Update File Path, Attached file, Mime Type for Image.
if ( 'full' === $size_k ) {
if ( ! empty( $meta ) ) {
$new_file = str_replace( $upload_path, '', $n_file_path );
$meta['file'] = $new_file;
if ( ! update_attached_file( $id, $meta['file'] ) ) {
'post_mime_type' => $mime,
$meta['sizes'][ $size_k ]['file'] = basename( $n_file );
$meta['sizes'][ $size_k ]['mime-type'] = $mime;
// To be called after the attached file key is updated for the image.
if ( ! $this->update_image_url( $id, $size_k, $n_file, $o_url ) ) {
* Delete the Original files if backup not enabled
* We only delete the file if we don't have any issues while updating the DB.
* SMUSH-1088?focusedCommentId=92914.
if ( $del_file && 'conversion' === $o_type ) {
// We might need to backup the full size file, will delete it later if we don't need to use it for backup.
if ( 'full' !== $size_k ) {
* We only need to keep the original file as a backup file.
* and try to delete this file on cloud too, e.g S3.
Helper::delete_permanently( $o_file, $id );
* Replace the file if there are savings, and return savings
* @param string $file Original File Path.
* @param array $result Array structure.
* @param string $n_file Updated File path.
private function replace_file( $file = '', $result = array(), $n_file = '' ) {
if ( empty( $file ) || empty( $n_file ) ) {
// Get the file size of original image.
$o_file_size = filesize( $file );
$n_file = path_join( dirname( $file ), $n_file );
$n_file_size = filesize( $n_file );
// If there aren't any savings return.
if ( $n_file_size >= $o_file_size ) {
// Delete the JPG image and return.
Helper::logger()->png2jpg()->notice( sprintf( 'The new file [%s](%s) is larger than the original file [%s](%s).', Helper::clean_file_path( $n_file ), size_format( $n_file_size ), Helper::clean_file_path( $file ), size_format( $o_file_size ) ) );
$savings = $o_file_size - $n_file_size;
'size_before' => $o_file_size,
'size_after' => $n_file_size,
$result['savings'] = $savings;
* Perform the conversion process, using WordPress Image Editor API
* @param string $id Attachment ID.
* @param string $file Attachment File path.
* @param string $meta Attachment meta.
* @param string $size Image size, default empty for full image.
* @return array $result array(
* 'meta' => array Update Attachment metadata
* 'savings' => Reduction of Image size in bytes
private function convert_to_jpg( $id = '', $file = '', $meta = '', $size = 'full' ) {
// Flag: Whether the image was converted or not.
if ( 'full' === $size ) {
$result['converted'] = false;
// If any of the values is not set.
if ( empty( $id ) || empty( $file ) || empty( $meta ) || ! file_exists( $file ) ) {
Helper::logger()->png2jpg()->info( sprintf( 'Meta file [%s(%d)] is empty or file not found.', Helper::clean_file_path( $file ), $id ) );
$editor = wp_get_image_editor( $file );
if ( is_wp_error( $editor ) ) {
// Use custom method maybe.
Helper::logger()->png2jpg()->error( sprintf( 'Image Editor cannot load file [%s(%d)]: %s.', Helper::clean_file_path( $file ), $id, $editor->get_error_message() ) );
$n_file = pathinfo( $file );
if ( ! empty( $n_file['filename'] ) && $n_file['dirname'] ) {
// Get a unique File name.
$file_detail = Helper::cache_get( $id, 'convert_to_jpg' );
list( $old_main_filename, $new_main_filename ) = $file_detail;
if ( $old_main_filename !== $new_main_filename ) {
$n_file['filename'] = str_replace( $old_main_filename, $new_main_filename, $n_file['filename'] );
$n_file['filename'] .= '.jpg';
$org_filename = $n_file['filename'];
* Get unique file name for the main file.
$n_file['filename'] = wp_unique_filename( $n_file['dirname'], $org_filename . '.jpg' );
Helper::cache_set( $id, array( $org_filename, pathinfo( $n_file['filename'], PATHINFO_FILENAME ) ), 'convert_to_jpg' );
$n_file = path_join( $n_file['dirname'], $n_file['filename'] );
Helper::logger()->png2jpg()->error( sprintf( 'Cannot retrieve the path info of file [%s(%d)].', Helper::clean_file_path( $file ), $id ) );
$new_image_info = $editor->save( $n_file, 'image/jpeg' );
// If image editor was unable to save the image, return.
if ( is_wp_error( $new_image_info ) ) {
$n_file = ! empty( $new_image_info ) ? $new_image_info['file'] : '';
// Replace file, and get savings.
$result = $this->replace_file( $file, $result, $n_file );
if ( ! empty( $result['savings'] ) ) {
if ( 'full' === $size ) {
$result['converted'] = true;
// Update the File Details. and get updated meta.