diff --git a/classes/Media/WP.php b/classes/Media/WP.php index 1bc3d7671..81bbebc23 100644 --- a/classes/Media/WP.php +++ b/classes/Media/WP.php @@ -1,6 +1,8 @@ filesystem = Imagify_Filesystem::get_instance(); + } + + /** + * Initialize the hooks. + */ + public function init() { + add_action( 'wr2x_before_regenerate', [ $this, 'restore_originally_uploaded_image' ] ); + add_action( 'wr2x_retina_file_added', [ $this, 'add_retina_size' ], 10, 3 ); + add_action( 'wr2x_generate_retina', [ $this, 'optimize_retina_sizes' ] ); + add_action( 'wr2x_generate_retina', [ $this, 'reset_optimized_full_size_image' ] ); + add_action( 'imagify_media_files', [ $this, 'add_retina_sizes_meta' ] ); + + add_action( 'wr2x_retina_file_removed', [ $this, 'remove_retina_webp_size' ], 10, 2 ); + add_action( 'wr2x_retina_file_removed', [ $this, 'remove_imagify_retina_data' ], 10, 2 ); + + add_action( 'wr2x_before_generate_thumbnails', [ $this, 'restore_originally_uploaded_image' ] ); + add_action( 'wr2x_generate_thumbnails', [ $this, 'reoptimize_regenerated_images' ] ); + add_action( 'wr2x_generate_thumbnails', [ $this, 'reset_optimized_full_size_image' ] ); + } + + /** + * Restore the optimized full-sized file and replace it by the original backup file. + * + * This is to have the original user-uploaded (rather than the optimized full-size) image in place + * when Perfect Images (re)generates any new images. + * + * @param int $media_id A media attachment ID. + */ + public function restore_originally_uploaded_image( int $media_id ) { + $media = new Media( $media_id ); + + if ( ! $this->can_restore_original( $media ) ) { + return; + } + + $fullsize_path = $media->get_raw_fullsize_path(); + $backup_path = $media->get_raw_backup_path(); + $tmp_file_path = $this->get_temporary_file_path( $fullsize_path ); + + if ( $this->filesystem->exists( $fullsize_path ) ) { + $this->filesystem->move( $fullsize_path, $tmp_file_path, true ); + } + + $this->filesystem->copy( $backup_path, $fullsize_path ); + } + + /** + * Add a newly generated retina size that will need to be optimized. + * + * @hooked wr2x_retina_file_added + * + * @param int $media_id The media attachment ID. + * @param string $retina_file The retina filename. + * @param string $size_name The size name. + */ + public function add_retina_size( int $media_id, string $retina_file, string $size_name ) { + $this->retina_sizes[] = [ + 'media_id' => $media_id, + 'retina_file' => $retina_file, + 'size_name' => $size_name, + ]; + } + + /** + * Optimize newly generated retina sizes. + * + * @hooked wr2x_generate_retina + * + * @param int $media_id The attachment id of the retina images to optimize. + */ + public function optimize_retina_sizes( int $media_id ) { + $process = new Process( new Data( new Media( $media_id ) ) ); + + // if this is a new upload, bail out -- we'll optimize everything after the upload completes. + if ( empty( $process->get_data()->get_optimization_data()['sizes'] ) ) { + return; + } + + $sizes = []; + + foreach ( $this->retina_sizes as $size ) { + if ( $media_id === $size['media_id'] ) { + $sizes[] = $size['size_name'] . '@2x'; + } + } + + if ( empty( $sizes ) ) { + return; + } + + $media_opt_level = $process->get_data()->get_optimization_level(); + $optimization_level = $media_opt_level ?: Imagify_Options::get_instance()->get( 'optimization_level' ); + + $process->optimize_sizes( $sizes, $optimization_level ); + } + + /** + * Filter a Media's get_media_files() response to include retina size data. + * + * @hooked imagify_media_files + * + * @param array $sizes The Media's size data. + * + * @return array Sizes data that includes retina sizes. + */ + public function add_retina_sizes_meta( array $sizes ): array { + if ( ! function_exists( 'wr2x_get_retina' ) ) { + return $sizes; + } + + foreach ( $sizes as $size => $size_data ) { + $retina_path = wr2x_get_retina( $size_data['path'] ); + + if ( empty( $retina_path ) ) { + continue; + } + + $sizes[ $size . '@2x' ] = [ + 'size' => $size . '@2x', + 'path' => $retina_path, + 'width' => $size_data['width'] * 2, + 'height' => $size_data['height'] * 2, + 'mime-type' => $size_data['mime-type'], + 'disabled' => false, + ]; + } + + return $sizes; + } + + /** + * Remove a retina-related webp file whose Perfect Images retina version has been deleted. + * + * @hooked wr2x_retina_file_removed + * + * @param int $media_id The media attachment ID. + * @param string $retina_file The retina filepath. + */ + public function remove_retina_webp_size( int $media_id, string $retina_file ) { + $meta = wp_get_attachment_metadata( $media_id ); + + $retina_webp_filepath = $this->get_retina_webp_filepath( $meta['file'], $retina_file ); + + if ( file_exists( $retina_webp_filepath ) ) { + unlink( $retina_webp_filepath ); + } + } + + /** + * Remove retina-related imagify data concerning a deleted Perfect Images retina file. + * + * @hooked wr2x_retina_file_removed + * + * @param int $media_id The media attachment ID. + * @param string $retina_file The retina filepath. + */ + public function remove_imagify_retina_data( int $media_id, string $retina_file ) { + $meta = wp_get_attachment_metadata( $media_id ); + $retina_file_info = pathinfo( $retina_file ); + $original_size_name = preg_replace( + '/@2x/', + '', + $retina_file_info['filename'] + ) . '.' . $retina_file_info['extension']; + + $imagify_size_names = $this->get_retina_imagify_data_size_names( $meta['sizes'], $original_size_name ); + + if ( empty( $imagify_size_names ) ) { + return; + } + + $imagify_data = new Data( new Media( $media_id ) ); + $imagify_data->delete_sizes_optimization_data( $imagify_size_names ); + } + + /** + * Reoptimize regenerated thumbnail images. + * + * @param int $media_id The attachment ID of the media being processed. + */ + public function reoptimize_regenerated_images( int $media_id ) { + $meta = wp_get_attachment_metadata( $media_id ); + $process = new Process( new Data( new Media( $media_id ) ) ); + + $sizes = isset( $meta['sizes'] ) && is_array( $meta['sizes'] ) ? $meta['sizes'] : []; + $media = $process->get_media(); + $fullsize_path = $media->get_raw_fullsize_path(); + + /** If full-size and original are not the same, we will need to re-optimize the full size, too. */ + if ( $fullsize_path && $media->get_original_path() !== $fullsize_path ) { + $sizes['full'] = []; + } + + if ( ! $sizes ) { + return; + } + + /** + * Optimize the sizes that have been regenerated. + */ + // If the media has WebP versions, recreate them for the sizes that have been regenerated. + $optimization_data = $process->get_data()->get_optimization_data(); + + if ( ! empty( $optimization_data['sizes'] ) ) { + $sizes = $this->add_webp_sizes( $optimization_data, $process, $sizes ); + } + + $sizes = array_keys( $sizes ); + $optimization_level = $process->get_data()->get_optimization_level(); + + // Delete related optimization data or nothing will be optimized. + $process->get_data()->delete_sizes_optimization_data( $sizes ); + $process->optimize_sizes( $sizes, $optimization_level ); + } + + /** + * Put the optimized full-sized file back. + * + * @param int $media_id A media attachment ID. + */ + public function reset_optimized_full_size_image( int $media_id ) { + $media = new Media( $media_id ); + + $file_path = $media->get_raw_original_path(); + $tmp_file_path = $this->get_temporary_file_path( $file_path ); + + if ( ! $this->filesystem->exists( $tmp_file_path ) ) { + return; + } + + $this->filesystem->move( $tmp_file_path, $file_path, true ); + } + + /** + * Check that we can restore an originally uploaded file to the full-size image path. + * + * To restore, all the following conditions must all be true: + * 1. We must have previously optimized the image, + * 2. We must have path info for the original, full-size, and backup paths, and + * 3. Original path is the same as the full-size math (otherwise, WP will create a new full-size from the original). + * + * @param Media $media An Imagify Media Instance. + * + * @return bool + */ + private function can_restore_original( Media $media ): bool { + $data = new Data( $media ); + $fullsize_path = $media->get_raw_fullsize_path(); + $original_path = $media->get_original_path(); + + return $data->is_optimized() && + ! empty( $fullsize_path ) && + ! empty( $original_path ) && + $fullsize_path === $original_path && + ! empty( $media->get_raw_backup_path() ); + } + + /** + * Get the retina webp filepath associated with a Perfect Images retina file. + * + * @param string $attachment_file The attachment file from WP's attachment meta. + * @param string $retina_file The retina file from Perfect Images. + * + * @return string The full retina-webp file path. + */ + private function get_retina_webp_filepath( string $attachment_file, string $retina_file ): string { + $pathinfo = pathinfo( $attachment_file ); + $directory = $pathinfo['dirname']; + $uploads = wp_upload_dir(); + $basedir = $uploads['basedir']; + + return trailingslashit( $basedir ) . trailingslashit( $directory ) . $retina_file . '.webp'; + } + + /** + * Get size names as used in an Imagify::AbstractData instance for retina images. + * + * Given an array of size data items from WP's attachment meta, + * and the filename of the original image derived from a Perfect Images retina filename, + * we get a list of size names for all retina-size images that will be found in Imagify's Data instance. + * + * @param array $sizes Sizes from WP's attachment meta. + * @param string $original_size_name The original image filename from which Perfect Images has created a retina filename. + * + * @return array A list of image size names related to the retina file in an Imagify Data Instance. + */ + private function get_retina_imagify_data_size_names( array $sizes, string $original_size_name ): array { + $imagify_size_names = []; + + foreach ( $sizes as $size => $size_data ) { + if ( $original_size_name === $size_data['file'] ) { + $imagify_size_names[] = $size . '@2x'; + $imagify_size_names[] = $size . '@2x@imagify-webp'; + } + } + + return $imagify_size_names; + } + + /** + * Get the path to the temporary file. + * + * @param string $file_path The optimized full-sized file path. + * + * @return string A temporary file path for the optimized full-sized file. + */ + private function get_temporary_file_path( string $file_path ): string { + return $file_path . '_backup'; + } + + /** + * Add webp sizes names to the sizes to be processed. + * + * @param array $optimization_data Optimization data. + * @param Process $process The Imagify Process instance. + * @param array $sizes The names of sizes to be processed. + * + * @return array Sizes array with any webp names added. + */ + private function add_webp_sizes( array $optimization_data, Process $process, array $sizes ): array { + foreach ( array_keys( $optimization_data['sizes'] ) as $size_name ) { + $non_webp_size_name = $process->is_size_webp( $size_name ); + + if ( ! $non_webp_size_name || ! isset( $sizes[ $non_webp_size_name ] ) ) { + continue; + } + + $sizes[ $size_name ] = []; + } + + return $sizes; + } +} diff --git a/inc/3rd-party/perfect-images/wp-retina-2x.php b/inc/3rd-party/perfect-images/wp-retina-2x.php new file mode 100755 index 000000000..e56897040 --- /dev/null +++ b/inc/3rd-party/perfect-images/wp-retina-2x.php @@ -0,0 +1,9 @@ +init(); diff --git a/inc/functions/common.php b/inc/functions/common.php index 680c27d65..ef72a3d75 100755 --- a/inc/functions/common.php +++ b/inc/functions/common.php @@ -55,7 +55,7 @@ function imagify_sanitize_context( $context ) { * @since 1.9 * @author Grégory Viguier * - * @param string $context The context name. Default values are 'wp' and 'custom-folders'. + * @param string $context The context name. Accepted values are 'wp' and 'custom-folders'. * @return ContextInterface The context instance. */ function imagify_get_context( $context ) { diff --git a/phpcs.xml b/phpcs.xml index aee22fdfc..df2c4046b 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -27,9 +27,9 @@ - - - + + +