feat: initial v0.1.0 MVP
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>
This commit is contained in:
112
includes/class-uploader-hook.php
Normal file
112
includes/class-uploader-hook.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?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,
|
||||
] );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user