settings = $settings; $this->logger = $logger; } /** * Register hooks. * * @return void */ public function register() { add_filter( 'wp_generate_attachment_metadata', array( $this, 'optimize_on_upload' ), 20, 2 ); add_filter( 'wp_get_attachment_image_src', array( $this, 'maybe_use_webp_src' ), 20, 4 ); } /** * Optimize image when uploaded. * * @param array $metadata Attachment metadata. * @param int $attachment_id Attachment ID. * @return array */ public function optimize_on_upload( $metadata, $attachment_id ) { if ( $this->settings->get( 'images.optimize_uploads', 1 ) ) { $this->optimize_attachment( $attachment_id, $metadata ); } return $metadata; } /** * Optimize a batch of unprocessed image attachments. * * @param int $limit Batch limit. * @return array */ public function optimize_batch( $limit = 20 ) { $limit = max( 1, absint( $limit ) ); $attachments = get_posts( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => array( 'image/jpeg', 'image/png' ), 'posts_per_page' => $limit, 'fields' => 'ids', 'meta_query' => array( array( 'key' => '_acelora_site_accelerator_optimized', 'compare' => 'NOT EXISTS', ), ), 'no_found_rows' => true, ) ); $result = array( 'processed' => 0, 'files' => 0, 'saved' => 0, 'webp' => 0, 'skipped' => 0, 'errors' => array(), ); foreach ( $attachments as $attachment_id ) { $item = $this->optimize_attachment( $attachment_id ); if ( ! empty( $item['files'] ) ) { ++$result['processed']; } $result['files'] += isset( $item['files'] ) ? absint( $item['files'] ) : 0; $result['saved'] += isset( $item['saved'] ) ? absint( $item['saved'] ) : 0; $result['webp'] += isset( $item['webp'] ) ? absint( $item['webp'] ) : 0; $result['skipped'] += isset( $item['skipped'] ) ? absint( $item['skipped'] ) : 0; if ( ! empty( $item['errors'] ) && is_array( $item['errors'] ) ) { $result['errors'] = array_merge( $result['errors'], $item['errors'] ); } } $this->logger->log( 'info', 'Image optimization batch complete.', $result ); return $result; } /** * Optimize all files for an attachment. * * @param int $attachment_id Attachment ID. * @param array|null $metadata Attachment metadata. * @return array */ public function optimize_attachment( $attachment_id, $metadata = null ) { $paths = $this->attachment_image_paths( $attachment_id, $metadata ); $result = array( 'saved' => 0, 'webp' => 0, 'files' => 0, 'skipped' => 0, 'errors' => array(), ); foreach ( $paths as $path ) { if ( ! $this->is_supported_file( $path ) ) { ++$result['skipped']; continue; } if ( ! wp_is_writable( $path ) ) { ++$result['skipped']; $result['errors'][] = basename( $path ) . ' is not writable.'; continue; } $result['saved'] += $this->optimize_image_file( $path ); ++$result['files']; if ( $this->settings->get( 'images.webp', 1 ) && $this->generate_webp( $path ) ) { ++$result['webp']; } } if ( $result['files'] > 0 ) { update_post_meta( $attachment_id, '_acelora_site_accelerator_optimized', time() ); $this->update_stats( 1, $result['saved'], $result['webp'] ); } elseif ( empty( $paths ) ) { $result['errors'][] = 'No local JPEG or PNG files were found for attachment ' . absint( $attachment_id ) . '.'; } return $result; } /** * Replace image source with WebP for browsers that accept it. * * @param array|false $image Image data. * @param int $attachment_id Attachment ID. * @param string|int[] $size Image size. * @param bool $icon Whether image is icon. * @return array|false */ public function maybe_use_webp_src( $image, $attachment_id, $size, $icon ) { if ( ! $image || $icon || ! $this->settings->get( 'images.webp', 1 ) ) { return $image; } $accepts = isset( $_SERVER['HTTP_ACCEPT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ) : ''; if ( false === stripos( $accepts, 'image/webp' ) ) { return $image; } $file = $this->url_to_file( $image[0] ); if ( ! $file || ! file_exists( $file ) ) { return $image; } $webp = $file . '.webp'; if ( file_exists( $webp ) ) { $image[0] = $this->file_to_url( $webp ); } return $image; } /** * Current stats. * * @return array */ public function stats() { return wp_parse_args( get_option( self::STATS_OPTION, array() ), array( 'optimized_images' => 0, 'saved_bytes' => 0, 'webp_files' => 0, ) ); } /** * Optimize an image file in place. * * @param string $file File path. * @return int Saved bytes. */ private function optimize_image_file( $file ) { $mime = wp_check_filetype( $file ); $old = file_exists( $file ) ? filesize( $file ) : 0; if ( ! wp_is_writable( $file ) ) { return 0; } $quality = max( 40, min( 100, absint( $this->settings->get( 'images.quality', 82 ) ) ) ); if ( 'image/jpeg' === $mime['type'] && function_exists( 'imagecreatefromjpeg' ) ) { $image = @imagecreatefromjpeg( $file ); if ( $image ) { imagejpeg( $image, $file, $quality ); imagedestroy( $image ); } } elseif ( 'image/png' === $mime['type'] && function_exists( 'imagecreatefrompng' ) ) { $image = @imagecreatefrompng( $file ); if ( $image ) { imagealphablending( $image, false ); imagesavealpha( $image, true ); imagepng( $image, $file, 6 ); imagedestroy( $image ); } } clearstatcache( true, $file ); $new = file_exists( $file ) ? filesize( $file ) : $old; return max( 0, $old - $new ); } /** * Generate WebP beside the original image. * * @param string $file File path. * @return bool */ private function generate_webp( $file ) { if ( ! function_exists( 'imagewebp' ) ) { return false; } $webp = $file . '.webp'; $dir = dirname( $webp ); if ( ! wp_is_writable( $dir ) ) { return false; } if ( file_exists( $webp ) && filemtime( $webp ) >= filemtime( $file ) ) { return false; } $mime = wp_check_filetype( $file ); $quality = max( 40, min( 100, absint( $this->settings->get( 'images.quality', 82 ) ) ) ); $image = false; if ( 'image/jpeg' === $mime['type'] && function_exists( 'imagecreatefromjpeg' ) ) { $image = @imagecreatefromjpeg( $file ); } elseif ( 'image/png' === $mime['type'] && function_exists( 'imagecreatefrompng' ) ) { $image = @imagecreatefrompng( $file ); } if ( ! $image ) { return false; } $result = imagewebp( $image, $webp, $quality ); imagedestroy( $image ); if ( $result && file_exists( $webp ) && filesize( $webp ) > 0 ) { if ( function_exists( 'wp_chmod_file' ) ) { wp_chmod_file( $webp ); } return true; } return false; } /** * Resolve all local image files for an attachment. * * @param int $attachment_id Attachment ID. * @param array|null $metadata Attachment metadata. * @return array */ private function attachment_image_paths( $attachment_id, $metadata = null ) { $paths = array(); $metadata = is_array( $metadata ) ? $metadata : wp_get_attachment_metadata( $attachment_id ); $file = get_attached_file( $attachment_id ); if ( ! $file && is_array( $metadata ) && ! empty( $metadata['file'] ) ) { $uploads = wp_get_upload_dir(); $file = trailingslashit( $uploads['basedir'] ) . $metadata['file']; } if ( $file && file_exists( $file ) ) { $paths[] = $file; } if ( is_array( $metadata ) && ! empty( $metadata['sizes'] ) && ! empty( $metadata['file'] ) && $file ) { $base = trailingslashit( dirname( $file ) ); foreach ( $metadata['sizes'] as $size ) { if ( ! empty( $size['file'] ) && file_exists( $base . $size['file'] ) ) { $paths[] = $base . $size['file']; } } } return array_values( array_unique( array_map( 'wp_normalize_path', $paths ) ) ); } /** * Check whether a file can be optimized by this module. * * @param string $file File path. * @return bool */ private function is_supported_file( $file ) { $mime = wp_check_filetype( $file ); return in_array( $mime['type'], array( 'image/jpeg', 'image/png' ), true ); } /** * Update saved stats. * * @param int $images Optimized image count. * @param int $saved Saved bytes. * @param int $webp WebP count. * @return void */ private function update_stats( $images, $saved, $webp ) { $stats = $this->stats(); $stats['optimized_images'] += absint( $images ); $stats['saved_bytes'] += absint( $saved ); $stats['webp_files'] += absint( $webp ); update_option( self::STATS_OPTION, $stats, false ); } /** * Convert file path to URL. * * @param string $file File path. * @return string */ private function file_to_url( $file ) { $uploads = wp_upload_dir(); $basedir = wp_normalize_path( $uploads['basedir'] ); $file = wp_normalize_path( $file ); if ( 0 === strpos( $file, $basedir ) ) { return trailingslashit( $uploads['baseurl'] ) . ltrim( substr( $file, strlen( $basedir ) ), '/' ); } return ''; } /** * Convert an uploads URL to a local file path. * * @param string $url Image URL. * @return string */ private function url_to_file( $url ) { $uploads = wp_upload_dir(); $baseurl = set_url_scheme( trailingslashit( $uploads['baseurl'] ) ); $url = set_url_scheme( strtok( $url, '?' ) ); if ( 0 !== strpos( $url, $baseurl ) ) { return ''; } $relative = ltrim( substr( $url, strlen( $baseurl ) ), '/' ); return wp_normalize_path( trailingslashit( $uploads['basedir'] ) . $relative ); } }