Replaces Smush Pro's optimisation pipeline without the grey-wash bug. CLI commands working: wp h4b-img status wp h4b-img optimise --id=<n> wp h4b-img bulk wp h4b-img rescue Verified on dev.rds.ink: - ICC profile preservation works (the Smush-bug fix) - Bulk: 20 attachments → 487 KB saved (10.4%), 0 errors - Rescue: end-to-end mechanism verified on WorkingAsOne_horse fixture - WebP synchronous, AVIF queued via WP-Cron - Originals backed up to wp-content/h4b-img-originals/ See CHANGELOG.md for details + ../DESIGN-h4b-image-optim.md for architecture. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
113 lines
3.7 KiB
PHP
113 lines
3.7 KiB
PHP
<?php
|
|
/**
|
|
* Hooks into the WP upload pipeline.
|
|
*
|
|
* Important: we hook into wp_generate_attachment_metadata so we run AFTER
|
|
* WP-core generates all the size variants. We then optimise each variant in
|
|
* place. This keeps WP's metadata structure intact.
|
|
*
|
|
* @package H4B\ImageOptim
|
|
*/
|
|
|
|
namespace H4B\ImageOptim;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
final class Uploader_Hook {
|
|
|
|
public static function register(): void {
|
|
add_filter( 'wp_generate_attachment_metadata', [ self::class, 'on_metadata' ], 10, 2 );
|
|
}
|
|
|
|
/**
|
|
* @param array $metadata The freshly-generated _wp_attachment_metadata array.
|
|
* @param int $attachment_id Post ID of the attachment.
|
|
*/
|
|
public static function on_metadata( $metadata, $attachment_id ): array {
|
|
$metadata = is_array( $metadata ) ? $metadata : [];
|
|
|
|
if ( ! Settings::get( 'optimise_on_upload', true ) ) {
|
|
return $metadata;
|
|
}
|
|
if ( Attachment_Meta::is_processed( (int) $attachment_id )
|
|
&& Settings::get( 'skip_already_processed', true ) ) {
|
|
return $metadata;
|
|
}
|
|
|
|
$uploads = wp_get_upload_dir();
|
|
$basedir = trailingslashit( $uploads['basedir'] );
|
|
|
|
// File path of the original full-size image
|
|
$relative = $metadata['file'] ?? get_post_meta( $attachment_id, '_wp_attached_file', true );
|
|
if ( ! $relative ) {
|
|
return $metadata;
|
|
}
|
|
$full_path = $basedir . $relative;
|
|
if ( ! is_readable( $full_path ) ) {
|
|
return $metadata;
|
|
}
|
|
|
|
// Optimise the full file
|
|
self::process_single( (int) $attachment_id, 'full', $full_path );
|
|
|
|
// Optimise every generated size
|
|
$dir = dirname( $full_path );
|
|
foreach ( ( $metadata['sizes'] ?? [] ) as $size_key => $size_data ) {
|
|
if ( empty( $size_data['file'] ) ) {
|
|
continue;
|
|
}
|
|
$size_path = trailingslashit( $dir ) . $size_data['file'];
|
|
if ( ! is_readable( $size_path ) ) {
|
|
continue;
|
|
}
|
|
self::process_single( (int) $attachment_id, (string) $size_key, $size_path );
|
|
// Refresh filesize in metadata after re-encode
|
|
clearstatcache( true, $size_path );
|
|
$metadata['sizes'][ $size_key ]['filesize'] = filesize( $size_path );
|
|
}
|
|
|
|
// Refresh full filesize too
|
|
clearstatcache( true, $full_path );
|
|
$metadata['filesize'] = filesize( $full_path );
|
|
|
|
return $metadata;
|
|
}
|
|
|
|
private static function process_single( int $attachment_id, string $size_key, string $path ): void {
|
|
$opt = Optimizer::optimise( $path );
|
|
|
|
// Generate WebP/AVIF siblings regardless of whether Optimizer touched
|
|
// the source JPEG (e.g. it might have skipped tiny thumbnails).
|
|
// Sibling generation is still valuable for those.
|
|
$webp_stats = [ 'status' => 'skipped' ];
|
|
$avif_stats = [ 'status' => 'skipped' ];
|
|
$generate_siblings = in_array( $opt['status'], [ 'done', 'skipped' ], true );
|
|
|
|
if ( $generate_siblings && Settings::get( 'generate_webp', true ) ) {
|
|
$webp_stats = Format_Generator::make_webp( $path );
|
|
}
|
|
if ( $generate_siblings && Settings::get( 'generate_avif', true ) ) {
|
|
$avif_stats = Format_Generator::make_avif( $path );
|
|
if ( $avif_stats['status'] === 'queued' ) {
|
|
Attachment_Meta::mark_avif_pending( $attachment_id, $size_key );
|
|
}
|
|
}
|
|
|
|
Attachment_Meta::record_size( $attachment_id, $size_key, [
|
|
'status' => $opt['status'],
|
|
'before' => $opt['before'],
|
|
'after' => $opt['after'],
|
|
'percent' => $opt['percent'],
|
|
'icc_preserved' => $opt['icc_preserved'] ?? false,
|
|
'tool_chain' => $opt['tool_chain'] ?? [],
|
|
'webp' => $webp_stats['status'] === 'done' ? $webp_stats['size'] : null,
|
|
'avif' => $avif_stats['status'] === 'done' ? $avif_stats['size'] : null,
|
|
'avif_status' => $avif_stats['status'],
|
|
'backup' => $opt['backup_path'] ?? null,
|
|
'error' => $opt['error'] ?? null,
|
|
] );
|
|
}
|
|
}
|