'image/webp' ) ); // imagewebp() and Imagick are enough to write WebP. The editor check only reports whether WordPress can also edit WebP files. $supported = $imagick || $gd || $editor; $label = ''; if ( $imagick ) { $label = 'Imagick'; } elseif ( $gd ) { $label = 'GD'; } $message = $supported ? '' : __( 'This server cannot create WebP files. Ask your host to enable Imagick, or GD with WebP support.', 'flipwebp' ); return array( 'supported' => $supported, 'label' => $label, 'message' => $message, ); } /** * Convert newly uploaded JPEG, PNG, and GIF files when the setting is on. * * @return void */ public static function register_upload_hook() { add_filter( 'wp_generate_attachment_metadata', array( self::class, 'convert_new_upload' ), 30, 2 ); } /** * Upload callback. It does nothing while a conversion is already running. * * @param mixed $metadata Attachment metadata. * @param int $attachment_id Attachment ID. * @return mixed */ public static function convert_new_upload( $metadata, $attachment_id ) { if ( self::$running || ! Settings::enabled( 'convert_on_upload' ) ) { return $metadata; } $mime = (string) get_post_mime_type( $attachment_id ); if ( ! in_array( $mime, array( 'image/jpeg', 'image/png', 'image/gif' ), true ) ) { return $metadata; } if ( ! Capabilities::can_convert( $attachment_id ) ) { return $metadata; } $result = ( new self() )->convert( (int) $attachment_id ); if ( is_wp_error( $result ) ) { return $metadata; } $fresh = wp_get_attachment_metadata( $attachment_id ); return ( is_array( $fresh ) && ! empty( $fresh ) ) ? $fresh : $metadata; } /** * Convert one attachment. * * @param int $attachment_id Attachment ID. * @return array|\WP_Error */ public function convert( $attachment_id ) { $attachment_id = (int) $attachment_id; $guard = $this->guard( $attachment_id ); if ( is_wp_error( $guard ) ) { return $guard; } if ( self::$running ) { return new \WP_Error( 'flipwebp_recursive', __( 'Image conversion is already running.', 'flipwebp' ) ); } $lock = 'flipwebp_lock_' . $attachment_id; if ( get_transient( $lock ) ) { return new \WP_Error( 'flipwebp_busy', __( 'This image is already being converted.', 'flipwebp' ) ); } set_transient( $lock, 1, 3 * MINUTE_IN_SECONDS ); self::$running = true; try { return $this->process( $attachment_id ); } finally { self::$running = false; delete_transient( $lock ); $this->remove_filters(); } } /** * Restore an attachment from its backup. * * @param int $attachment_id Attachment ID. * @return array|\WP_Error */ public function restore( $attachment_id ) { $attachment_id = (int) $attachment_id; $guard = $this->guard( $attachment_id ); if ( is_wp_error( $guard ) ) { return $guard; } if ( self::$running ) { return new \WP_Error( 'flipwebp_recursive', __( 'Image conversion is already running.', 'flipwebp' ) ); } $lock = 'flipwebp_lock_' . $attachment_id; if ( get_transient( $lock ) ) { return new \WP_Error( 'flipwebp_busy', __( 'This image is already being converted.', 'flipwebp' ) ); } set_transient( $lock, 1, 3 * MINUTE_IN_SECONDS ); self::$running = true; try { return $this->process_restore( $attachment_id ); } finally { self::$running = false; delete_transient( $lock ); } } /** * Force WebP output while this request is converting. * * @param mixed $formats Current output map. * @return array */ public function filter_output_format( $formats ) { if ( ! is_array( $formats ) ) { $formats = array(); } $formats['image/jpeg'] = 'image/webp'; $formats['image/png'] = 'image/webp'; $formats['image/gif'] = 'image/webp'; $formats['image/webp'] = 'image/webp'; return $formats; } /** * Apply the saved quality to the image editor and its generated sizes. * * @param int $quality Quality proposed by WordPress. * @param string $mime_type Output MIME type. * @param array $dims Dimensions. * @return int */ public function filter_editor_quality( $quality, $mime_type = '', $dims = array() ) { unset( $quality, $mime_type, $dims ); return (int) $this->quality; } /** * @param int $attachment_id Attachment ID. * @return true|\WP_Error */ private function guard( $attachment_id ) { if ( ! Capabilities::can_convert( $attachment_id ) ) { return new \WP_Error( 'flipwebp_forbidden', __( 'You do not have permission to convert this image.', 'flipwebp' ), array( 'status' => 403 ) ); } $post = get_post( $attachment_id ); if ( ! $post instanceof \WP_Post || 'attachment' !== $post->post_type || 'trash' === $post->post_status ) { return new \WP_Error( 'flipwebp_missing', __( 'That media file could not be found.', 'flipwebp' ) ); } return true; } /** * @param int $attachment_id Attachment ID. * @return array|\WP_Error */ private function process( $attachment_id ) { $this->raise_limits(); $source = $this->source_file( $attachment_id ); if ( is_wp_error( $source ) ) { return $source; } if ( $this->is_animated( $source ) ) { update_post_meta( $attachment_id, '_flipwebp_status', 'skipped' ); update_post_meta( $attachment_id, '_flipwebp_skip_reason', 'animated' ); Stats::flush(); return array( 'status' => 'skipped', 'message' => __( 'Animated images are left unchanged so the animation is preserved.', 'flipwebp' ), ); } $dimensions = wp_getimagesize( $source ); if ( ! is_array( $dimensions ) || empty( $dimensions[0] ) || empty( $dimensions[1] ) ) { return new \WP_Error( 'flipwebp_invalid_image', __( 'The image could not be read.', 'flipwebp' ) ); } $width = (int) $dimensions[0]; $height = (int) $dimensions[1]; $pixels = (float) $width * (float) $height; $decode = $this->shrink_to_fit( $attachment_id, $source, $width, $height, $pixels ); if ( is_wp_error( $decode ) ) { return $decode; } $engine = self::engine(); if ( empty( $engine['supported'] ) ) { $this->delete_reduced_files(); return new \WP_Error( 'flipwebp_no_webp', $engine['message'] ); } $old_files = Library::files( $attachment_id ); $old_total = Filesystem::total_size( $old_files ); if ( $old_total < 1 ) { $this->delete_reduced_files(); return new \WP_Error( 'flipwebp_missing_file', __( 'The original file is missing from the server.', 'flipwebp' ) ); } $state = $this->capture_state( $attachment_id ); $old_urls = $this->location_map( $attachment_id ); $snapshot = Backup::snapshot( $attachment_id, $old_files, $state ); if ( is_wp_error( $snapshot ) ) { $this->delete_reduced_files(); return $snapshot; } $created = array(); try { $this->add_filters(); $encoded = $this->encode( $attachment_id, $decode, $source ); if ( is_wp_error( $encoded ) ) { Backup::discard( $snapshot ); return $encoded; } $created[] = $encoded['path']; $active = $encoded['path']; if ( $encoded['replace'] ) { if ( ! Filesystem::copy_file( $encoded['path'], $source ) ) { $this->rollback( $attachment_id, $snapshot, $state, $created, $old_files ); Backup::discard( $snapshot ); return new \WP_Error( 'flipwebp_write', __( 'The WebP image could not be saved.', 'flipwebp' ) ); } wp_delete_file( $encoded['path'] ); $created = array( $source ); $active = $source; } $this->point_attachment( $attachment_id, $active, 'image/webp' ); if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; } $metadata = wp_generate_attachment_metadata( $attachment_id, $active ); if ( ! is_array( $metadata ) || empty( $metadata['width'] ) || empty( $metadata['height'] ) ) { $this->rollback( $attachment_id, $snapshot, $state, Library::files( $attachment_id ), $old_files ); Backup::discard( $snapshot ); return new \WP_Error( 'flipwebp_metadata', __( 'The WebP image was created, but its sizes could not be generated. The original file was restored.', 'flipwebp' ) ); } wp_update_attachment_metadata( $attachment_id, $metadata ); $new_files = Library::files( $attachment_id ); $new_total = Filesystem::total_size( $new_files ); $saved = $old_total - $new_total; $percent = ( $saved / $old_total ) * 100; if ( $saved < 1024 || $percent < 1 ) { $this->rollback( $attachment_id, $snapshot, $state, $new_files, $old_files ); $this->mark_not_smaller( $attachment_id ); Backup::discard( $snapshot ); Stats::flush(); return array( 'status' => 'skipped', 'message' => __( 'The WebP version was not meaningfully smaller, so the original file was kept.', 'flipwebp' ), ); } $pairs = $this->url_pairs( $old_urls, $this->location_map( $attachment_id ) ); $replace = Replacer::replace( $attachment_id, $pairs ); $warnings = array(); $clean = true; if ( is_wp_error( $replace ) ) { $clean = false; $warnings[] = $replace->get_error_message(); } else { $warnings = $replace['warnings']; $clean = empty( $warnings ); } $persisted = true; if ( Settings::enabled( 'create_backups' ) ) { $persisted = Backup::persist( $attachment_id, $snapshot, $pairs ); if ( ! $persisted ) { $warnings[] = __( 'The WebP image was saved, but the original could not be copied to the backup folder.', 'flipwebp' ); } } $delete = $clean && $persisted && Settings::enabled( 'delete_originals' ); if ( $delete ) { clearstatcache(); Filesystem::delete_files( $old_files, array_keys( $this->normalized_realpaths( $new_files ) ) ); } Backup::discard( $snapshot ); $this->store_result( $attachment_id, $old_total, $new_total, $delete ); Stats::flush(); $message = $this->success_message( $saved, $percent, $delete ); if ( ! empty( $warnings ) ) { $message .= ' ' . $warnings[0]; } $result = array( 'status' => 'converted', 'message' => $message, 'warnings' => $warnings, ); /** * Fires after an attachment has been converted to WebP. * * @param int $attachment_id Attachment ID. * @param array $result Public result. */ do_action( 'flipwebp_converted', $attachment_id, $result ); return $result; } catch ( \Throwable $exception ) { unset( $exception ); $orphan_files = array_merge( $created, Library::files( $attachment_id ) ); $this->rollback( $attachment_id, $snapshot, $state, $orphan_files, $old_files ); Backup::discard( $snapshot ); return new \WP_Error( 'flipwebp_failed', __( 'The image could not be converted. The original file was left in place.', 'flipwebp' ) ); } finally { $this->delete_reduced_files(); } } /** * @param int $attachment_id Attachment ID. * @return array|\WP_Error */ private function process_restore( $attachment_id ) { $manifest = Backup::restore_files( $attachment_id ); if ( is_wp_error( $manifest ) ) { return $manifest; } $current = Library::files( $attachment_id ); $reverse = array(); foreach ( $manifest['urls'] as $pair ) { $reverse[] = array( 'old' => $pair['new'], 'new' => $pair['old'], ); } $replacement = Replacer::replace( $attachment_id, $reverse ); $clean = ! is_wp_error( $replacement ) && empty( $replacement['warnings'] ); $restored_file = Filesystem::absolute_from_relative( $manifest['attached_file'] ); if ( '' === $restored_file ) { $restored_file = $manifest['attached_file']; } $this->point_attachment( $attachment_id, $restored_file, $manifest['mime'] ); wp_update_attachment_metadata( $attachment_id, $manifest['metadata'] ); if ( $clean ) { $keep = array(); foreach ( $manifest['files'] as $file ) { $path = Filesystem::absolute_from_relative( $file['relative'] ); if ( '' !== $path ) { $keep[] = $path; } } Filesystem::delete_files( $current, array_keys( $this->normalized_realpaths( $keep ) ) ); } delete_post_meta( $attachment_id, '_flipwebp_status' ); delete_post_meta( $attachment_id, '_flipwebp_skip_reason' ); delete_post_meta( $attachment_id, '_flipwebp_original_bytes' ); delete_post_meta( $attachment_id, '_flipwebp_webp_bytes' ); delete_post_meta( $attachment_id, '_flipwebp_saved_bytes' ); delete_post_meta( $attachment_id, '_flipwebp_converted_at' ); Stats::flush(); $message = __( 'The original image was restored.', 'flipwebp' ); if ( ! $clean ) { $message .= ' ' . __( 'Some content references could not be changed, so the WebP files were also kept.', 'flipwebp' ); } return array( 'status' => 'restored', 'message' => $message, ); } /** * Encode the source image to a WebP file in the same directory. * * @param int $attachment_id Attachment ID. * @param string $source Image to decode. May be a reduced copy. * @param string $name_from Original file whose name and folder are kept. * @return array{path: string, replace: bool}|\WP_Error */ private function encode( $attachment_id, $source, $name_from = '' ) { if ( ! function_exists( 'wp_unique_filename' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( '' === $name_from ) { $name_from = $source; } $this->quality = Settings::quality( $attachment_id ); $editor = wp_get_image_editor( $source ); if ( is_wp_error( $editor ) ) { return new \WP_Error( 'flipwebp_editor', __( 'The image could not be opened.', 'flipwebp' ) ); } $rotated = $editor->maybe_exif_rotate(); if ( is_wp_error( $rotated ) ) { return new \WP_Error( 'flipwebp_rotate', __( 'The image could not be rotated into the correct orientation.', 'flipwebp' ) ); } $max = Settings::max_dimension( $attachment_id ); if ( $max > 0 ) { $size = $editor->get_size(); if ( is_array( $size ) && ( (int) $size['width'] > $max || (int) $size['height'] > $max ) ) { $resized = $editor->resize( $max, $max, false ); if ( is_wp_error( $resized ) ) { return new \WP_Error( 'flipwebp_resize', __( 'The image could not be resized.', 'flipwebp' ) ); } } } $editor->set_quality( $this->quality ); $directory = dirname( $name_from ); $filename = wp_basename( $name_from ); $extension = strtolower( (string) pathinfo( $filename, PATHINFO_EXTENSION ) ); $stem = wp_basename( $filename, '.' . $extension ); $replace = ( 'webp' === $extension ); $target = $replace ? $stem . '-flipwebp-tmp.webp' : $stem . '.webp'; $unique = wp_unique_filename( $directory, $target ); if ( ! Filesystem::is_safe_basename( $unique ) || 'webp' !== strtolower( (string) pathinfo( $unique, PATHINFO_EXTENSION ) ) ) { return new \WP_Error( 'flipwebp_filename', __( 'A safe WebP file name could not be created.', 'flipwebp' ) ); } $dest = path_join( $directory, $unique ); $saved = $editor->save( $dest, 'image/webp' ); if ( is_wp_error( $saved ) || empty( $saved['path'] ) || empty( $saved['mime-type'] ) ) { if ( is_file( $dest ) ) { wp_delete_file( $dest ); } return new \WP_Error( 'flipwebp_encode', __( 'The image could not be converted to WebP.', 'flipwebp' ) ); } $path = (string) $saved['path']; if ( 'image/webp' !== $saved['mime-type'] || 'image/webp' !== wp_get_image_mime( $path ) || ! Filesystem::is_inside_uploads( $path ) ) { if ( is_file( $path ) ) { wp_delete_file( $path ); } return new \WP_Error( 'flipwebp_encode', __( 'The image could not be converted to WebP.', 'flipwebp' ) ); } $saved_dir = realpath( dirname( $path ) ); $source_dir = realpath( $directory ); if ( false === $saved_dir || false === $source_dir || Filesystem::normalize( $saved_dir ) !== Filesystem::normalize( $source_dir ) ) { wp_delete_file( $path ); return new \WP_Error( 'flipwebp_encode', __( 'The image could not be converted to WebP.', 'flipwebp' ) ); } $size = filesize( $path ); if ( false === $size || $size < 1 ) { wp_delete_file( $path ); return new \WP_Error( 'flipwebp_encode', __( 'The image could not be converted to WebP.', 'flipwebp' ) ); } return array( 'path' => $path, 'replace' => $replace, ); } /** * Choose the best local source file for an attachment. * * @param int $attachment_id Attachment ID. * @return string|\WP_Error */ private function source_file( $attachment_id ) { $file = get_attached_file( $attachment_id ); if ( ! is_string( $file ) || '' === $file || 1 === preg_match( '#^https?://#i', $file ) ) { return new \WP_Error( 'flipwebp_remote', __( 'This file is not stored in the local uploads folder, so it cannot be converted.', 'flipwebp' ) ); } if ( function_exists( 'wp_get_original_image_path' ) ) { $original = wp_get_original_image_path( $attachment_id ); if ( is_string( $original ) && '' !== $original && $original !== $file && $this->is_local_image( $original ) ) { $file = $original; } } if ( ! $this->is_local_image( $file ) ) { if ( ! is_file( $file ) ) { return new \WP_Error( 'flipwebp_missing_file', __( 'The original file is missing from the server.', 'flipwebp' ) ); } return new \WP_Error( 'flipwebp_invalid_type', __( 'Only JPEG, PNG, GIF, and WebP images in the uploads folder can be converted.', 'flipwebp' ) ); } return $file; } /** * @param string $file Absolute path. * @return bool */ private function is_local_image( $file ) { if ( ! is_string( $file ) || ! is_file( $file ) || is_link( $file ) || ! Filesystem::is_inside_uploads( $file ) ) { return false; } $extension = strtolower( (string) pathinfo( $file, PATHINFO_EXTENSION ) ); if ( ! in_array( $extension, array( 'jpg', 'jpeg', 'png', 'gif', 'webp' ), true ) ) { return false; } $checked = wp_check_filetype_and_ext( $file, wp_basename( $file ) ); $type = ( is_array( $checked ) && ! empty( $checked['type'] ) ) ? (string) $checked['type'] : ''; if ( '' === $type ) { $type = (string) wp_get_image_mime( $file ); } return self::is_manageable_mime( $type ); } /** * @param string $file Absolute path. * @return bool */ private function is_animated( $file ) { $mime = (string) wp_get_image_mime( $file ); if ( 'image/gif' === $mime ) { return $this->gif_has_multiple_frames( $file ); } if ( 'image/webp' === $mime ) { return $this->file_contains( $file, array( 'ANIM', 'ANMF' ) ); } return false; } /** * @param string $file GIF path. * @return bool */ private function gif_has_multiple_frames( $file ) { $size = filesize( $file ); if ( false === $size || $size > 20 * MB_IN_BYTES ) { return true; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local GIF frame scan, capped at 20 MB above. $contents = file_get_contents( $file ); if ( ! is_string( $contents ) ) { return true; } return substr_count( $contents, "\x00\x21\xF9\x04" ) > 1; } /** * @param string $file File path. * @param string[] $needles Byte sequences. * @return bool */ private function file_contains( $file, $needles ) { $handle = fopen( $file, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen if ( false === $handle ) { return true; } $carry = ''; $read = 0; $limit = 8 * MB_IN_BYTES; while ( ! feof( $handle ) && $read < $limit ) { $chunk = fread( $handle, MB_IN_BYTES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread if ( ! is_string( $chunk ) || '' === $chunk ) { break; } $read += strlen( $chunk ); $haystack = $carry . $chunk; foreach ( $needles as $needle ) { if ( false !== strpos( $haystack, $needle ) ) { fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose return true; } } $carry = substr( $haystack, -16 ); } fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose return false; } /** * @param int $attachment_id Attachment ID. * @return array{mime: string, attached_file: string, metadata: array} */ private function capture_state( $attachment_id ) { $metadata = wp_get_attachment_metadata( $attachment_id ); $attached = get_post_meta( $attachment_id, '_wp_attached_file', true ); return array( 'mime' => (string) get_post_mime_type( $attachment_id ), 'attached_file' => is_string( $attached ) ? $attached : '', 'metadata' => is_array( $metadata ) ? $metadata : array(), ); } /** * @param int $attachment_id Attachment ID. * @param string $file Absolute file. * @return void */ private function point_attachment( $attachment_id, $file, $mime ) { if ( is_string( $file ) && '' !== $file ) { update_attached_file( $attachment_id, $file ); } wp_update_post( array( 'ID' => $attachment_id, 'post_mime_type' => $mime, ), false, false ); } /** * @param int $attachment_id Attachment ID. * @param string $snapshot Snapshot directory. * @param array{mime: string, attached_file: string, metadata: array} $state Previous state. * @param string[] $new_files Files created by the attempt. * @param string[] $old_files Files that belong to the original. * @return void */ private function rollback( $attachment_id, $snapshot, $state, $new_files, $old_files ) { Backup::restore_snapshot( $snapshot ); if ( '' !== $state['attached_file'] ) { update_attached_file( $attachment_id, $state['attached_file'] ); } wp_update_post( array( 'ID' => $attachment_id, 'post_mime_type' => $state['mime'], ), false, false ); if ( ! empty( $state['metadata'] ) ) { wp_update_attachment_metadata( $attachment_id, $state['metadata'] ); } Filesystem::delete_files( $new_files, array_keys( $this->normalized_realpaths( $old_files ) ) ); } /** * @param int $attachment_id Attachment ID. * @return void */ private function mark_not_smaller( $attachment_id ) { if ( 'converted' === (string) get_post_meta( $attachment_id, '_flipwebp_status', true ) ) { return; } update_post_meta( $attachment_id, '_flipwebp_status', 'skipped' ); update_post_meta( $attachment_id, '_flipwebp_skip_reason', 'larger' ); } /** * @param int $attachment_id Attachment ID. * @param int $old_total Previous total bytes. * @param int $new_total New total bytes. * @param bool $deleted Whether the originals were removed. * @return void */ private function store_result( $attachment_id, $old_total, $new_total, $deleted ) { if ( $deleted ) { $baseline = (int) get_post_meta( $attachment_id, '_flipwebp_original_bytes', true ); if ( $baseline < 1 ) { $baseline = $old_total; update_post_meta( $attachment_id, '_flipwebp_original_bytes', $baseline ); } update_post_meta( $attachment_id, '_flipwebp_saved_bytes', max( 0, $baseline - $new_total ) ); } else { update_post_meta( $attachment_id, '_flipwebp_saved_bytes', 0 ); } update_post_meta( $attachment_id, '_flipwebp_webp_bytes', max( 0, $new_total ) ); update_post_meta( $attachment_id, '_flipwebp_status', 'converted' ); update_post_meta( $attachment_id, '_flipwebp_converted_at', time() ); delete_post_meta( $attachment_id, '_flipwebp_skip_reason' ); } /** * @param int $saved Bytes saved. * @param float $percent Percentage saved. * @param bool $deleted Whether originals were deleted. * @return string */ private function success_message( $saved, $percent, $deleted ) { $amount = size_format( max( 0, (int) $saved ), 1 ); $percent = number_format_i18n( max( 0, $percent ), 0 ); if ( $deleted ) { /* translators: 1: saved file size, 2: percentage saved. */ return sprintf( __( 'Converted to WebP and saved %1$s (%2$s%%).', 'flipwebp' ), $amount, $percent ); } /* translators: 1: saved file size, 2: percentage saved. */ return sprintf( __( 'Converted to WebP. The new file is %1$s (%2$s%%) smaller, and the original file was kept.', 'flipwebp' ), $amount, $percent ); } /** * @param int $attachment_id Attachment ID. * @return array */ private function location_map( $attachment_id ) { $map = array(); $full = wp_get_attachment_url( $attachment_id ); $relative = get_post_meta( $attachment_id, '_wp_attached_file', true ); $uploads = wp_get_upload_dir(); if ( is_string( $full ) && '' !== $full ) { $map['full'] = $full; } if ( is_string( $relative ) && '' !== $relative && ! empty( $uploads['baseurl'] ) ) { $map['local'] = trailingslashit( (string) $uploads['baseurl'] ) . ltrim( str_replace( '\\', '/', $relative ), '/' ); } if ( function_exists( 'wp_get_original_image_url' ) ) { $original = wp_get_original_image_url( $attachment_id ); if ( is_string( $original ) && '' !== $original ) { $map['original'] = $original; } } $metadata = wp_get_attachment_metadata( $attachment_id ); if ( is_array( $metadata ) && ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) { $base = ''; if ( ! empty( $map['local'] ) ) { $slash = strrpos( $map['local'], '/' ); $base = false === $slash ? '' : substr( $map['local'], 0, $slash + 1 ); } foreach ( $metadata['sizes'] as $name => $size ) { if ( ! is_string( $name ) || ! is_array( $size ) ) { continue; } $src = wp_get_attachment_image_src( $attachment_id, $name ); if ( is_array( $src ) && ! empty( $src[0] ) && is_string( $src[0] ) ) { $map[ 'size:' . $name ] = $src[0]; } if ( '' !== $base && ! empty( $size['file'] ) && is_string( $size['file'] ) ) { $map[ 'local-size:' . $name ] = $base . $size['file']; } } } return $map; } /** * @param array $old Old URLs. * @param array $new New URLs. * @return array> */ private function url_pairs( $old, $new ) { $pairs = array(); foreach ( $old as $key => $url ) { $replacement = ''; if ( isset( $new[ $key ] ) ) { $replacement = $new[ $key ]; } elseif ( isset( $new['full'] ) ) { $replacement = $new['full']; } if ( '' === $replacement || $url === $replacement ) { continue; } $pairs[] = array( 'old' => $url, 'new' => $replacement, ); } return $pairs; } /** * @param string[] $files Absolute paths. * @return array */ private function normalized_realpaths( $files ) { $set = array(); foreach ( $files as $file ) { if ( ! is_string( $file ) || '' === $file ) { continue; } $real = realpath( $file ); if ( false !== $real ) { $set[ Filesystem::normalize( $real ) ] = true; } } return $set; } /** * @return void */ private function add_filters() { add_filter( 'wp_editor_set_quality', array( $this, 'filter_editor_quality' ), 9999, 3 ); add_filter( 'image_editor_output_format', array( $this, 'filter_output_format' ), 9999 ); } /** * @return void */ private function remove_filters() { remove_filter( 'wp_editor_set_quality', array( $this, 'filter_editor_quality' ), 9999 ); remove_filter( 'image_editor_output_format', array( $this, 'filter_output_format' ), 9999 ); } /** * @return void */ private function raise_limits() { wp_raise_memory_limit( 'image' ); if ( function_exists( 'wp_is_ini_value_changeable' ) && wp_is_ini_value_changeable( 'memory_limit' ) ) { $current = wp_convert_hr_to_bytes( (string) ini_get( 'memory_limit' ) ); $target = 1024 * MB_IN_BYTES; if ( $current >= 0 && $current < $target ) { ini_set( 'memory_limit', '1024M' ); } } if ( function_exists( 'set_time_limit' ) ) { set_time_limit( 120 ); } if ( function_exists( 'ignore_user_abort' ) ) { ignore_user_abort( true ); } } /** * Use a smaller copy when the full image cannot be decoded in the available memory. * * @param int $attachment_id Attachment ID. * @param string $source Original file. * @param int $width Width in pixels. * @param int $height Height in pixels. * @param float $pixels Width multiplied by height. * @return string|\WP_Error */ private function shrink_to_fit( $attachment_id, $source, $width, $height, $pixels ) { $max = $this->max_pixels(); if ( $pixels <= $max ) { return $source; } $scale = sqrt( $max / $pixels ); $target_w = max( 1, (int) floor( $width * $scale ) ); $target_h = max( 1, (int) floor( $height * $scale ) ); $reduced = $this->write_reduced_copy( $source, $target_w, $target_h ); if ( '' !== $reduced ) { return $reduced; } $existing = $this->largest_loaded_file( $attachment_id, $source, max( $max, 12000000 ) ); if ( '' !== $existing ) { return $existing; } $any = $this->largest_loaded_file( $attachment_id, $source, PHP_INT_MAX, 1 ); if ( '' !== $any ) { return $any; } return $source; } /** * Write a smaller sibling file with Imagick, which can read JPEGs at a reduced size. * * @param string $source Original file. * @param int $target_w Target width. * @param int $target_h Target height. * @return string Empty when Imagick cannot write the copy. */ private function write_reduced_copy( $source, $target_w, $target_h ) { if ( ! extension_loaded( 'imagick' ) || ! class_exists( '\Imagick' ) ) { return ''; } if ( ! function_exists( 'wp_unique_filename' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $directory = dirname( $source ); if ( ! Filesystem::is_inside_uploads( $directory ) ) { return ''; } $extension = strtolower( (string) pathinfo( $source, PATHINFO_EXTENSION ) ); $out_ext = in_array( $extension, array( 'png', 'gif', 'webp' ), true ) ? $extension : 'jpg'; $name = wp_unique_filename( $directory, 'flipwebp-work-' . wp_generate_password( 8, false, false ) . '.' . $out_ext ); if ( ! Filesystem::is_safe_basename( $name ) ) { return ''; } $dest = path_join( $directory, $name ); try { $image = new \Imagick(); if ( in_array( $extension, array( 'jpg', 'jpeg' ), true ) ) { $image->setOption( 'jpeg:size', $target_w . 'x' . $target_h ); } $image->readImage( $source ); $image->thumbnailImage( $target_w, $target_h, true ); $image->setImagePage( 0, 0, 0, 0 ); if ( 'jpg' === $out_ext ) { $image->setImageBackgroundColor( 'white' ); if ( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ) { $image->setImageAlphaChannel( \Imagick::ALPHACHANNEL_REMOVE ); } $image->setImageFormat( 'jpeg' ); } $image->writeImage( $dest ); $image->clear(); $image->destroy(); } catch ( \Throwable $exception ) { unset( $exception ); if ( is_file( $dest ) ) { wp_delete_file( $dest ); } return ''; } if ( ! is_file( $dest ) || filesize( $dest ) < 1 || ! Filesystem::is_inside_uploads( $dest ) ) { if ( is_file( $dest ) ) { wp_delete_file( $dest ); } return ''; } $this->reduced_files[] = $dest; return $dest; } /** * Largest derivative WordPress already created that fits in the pixel budget. * * @param int $attachment_id Attachment ID. * @param string $original Full-size file. * @param int $max_pixels Pixel budget. * @param int $min_edge Smallest long edge to accept. * @return string */ private function largest_loaded_file( $attachment_id, $original, $max_pixels, $min_edge = 256 ) { $paths = array(); $main = get_attached_file( $attachment_id ); if ( is_string( $main ) && '' !== $main && $main !== $original ) { $paths[] = $main; } $meta = wp_get_attachment_metadata( $attachment_id ); if ( is_array( $meta ) && ! empty( $meta['sizes'] ) && is_array( $meta['sizes'] ) ) { $dir = dirname( is_string( $main ) && '' !== $main ? $main : $original ); foreach ( $meta['sizes'] as $size ) { if ( ! is_array( $size ) || empty( $size['file'] ) ) { continue; } $paths[] = path_join( $dir, (string) $size['file'] ); } } $best = ''; $best_pixels = 0; foreach ( $paths as $path ) { if ( ! $this->is_local_image( $path ) ) { continue; } $size = wp_getimagesize( $path ); if ( ! is_array( $size ) || empty( $size[0] ) || empty( $size[1] ) ) { continue; } $count = (int) $size[0] * (int) $size[1]; $long = max( (int) $size[0], (int) $size[1] ); if ( $long < $min_edge || $count > $max_pixels || $count <= $best_pixels ) { continue; } $best = $path; $best_pixels = $count; } return $best; } /** * @return void */ private function delete_reduced_files() { foreach ( $this->reduced_files as $file ) { if ( is_string( $file ) && is_file( $file ) ) { wp_delete_file( $file ); } } $this->reduced_files = array(); } /** * Pixel budget based on the PHP memory limit. * * About eight bytes per pixel covers the decoded image and one working buffer. * * @return int */ private function max_pixels() { $limit = wp_convert_hr_to_bytes( (string) ini_get( 'memory_limit' ) ); if ( $limit <= 0 || $limit > 2 * GB_IN_BYTES ) { $pixels = 120000000; } else { $available = $limit - memory_get_usage( true ) - ( 32 * MB_IN_BYTES ); if ( $available < 64 * MB_IN_BYTES ) { $available = 64 * MB_IN_BYTES; } $pixels = (int) floor( $available / 6 ); if ( $limit >= 256 * MB_IN_BYTES ) { $pixels = max( $pixels, 24000000 ); } } /** * Filters the maximum pixel count FlipWebP will decode. * * @param int $pixels Maximum pixels. */ $pixels = (int) apply_filters( 'flipwebp_max_pixels', $pixels ); return min( 120000000, max( 1000000, $pixels ) ); } }