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>
181 lines
5.2 KiB
PHP
181 lines
5.2 KiB
PHP
<?php
|
|
/**
|
|
* WP-CLI command: wp h4b-img <sub>
|
|
*
|
|
* @package H4B\ImageOptim
|
|
*/
|
|
|
|
namespace H4B\ImageOptim;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
final class CLI {
|
|
|
|
public static function register(): void {
|
|
\WP_CLI::add_command( 'h4b-img', __CLASS__ );
|
|
\WP_CLI::add_command( 'h4b-img bulk', CLI_Bulk::class );
|
|
\WP_CLI::add_command( 'h4b-img rescue', CLI_Rescue::class );
|
|
}
|
|
|
|
/**
|
|
* Show tool detection + plugin status.
|
|
*
|
|
* ## EXAMPLES
|
|
* wp h4b-img status
|
|
*/
|
|
public function status( $args, $assoc ): void {
|
|
$tools = Tools::detect();
|
|
\WP_CLI::log( 'h4b-image-optim v' . H4B_IMG_OPTIM_VERSION );
|
|
\WP_CLI::log( '' );
|
|
\WP_CLI::log( 'External tools:' );
|
|
foreach ( $tools as $name => $path ) {
|
|
$mark = $path ? '✓' : '✗';
|
|
\WP_CLI::log( sprintf( ' %s %-12s %s', $mark, $name, $path ?: '(not found)' ) );
|
|
}
|
|
$missing = Tools::missing_required();
|
|
if ( ! empty( $missing ) ) {
|
|
\WP_CLI::warning( 'Missing required tools: ' . implode( ', ', $missing ) );
|
|
}
|
|
|
|
\WP_CLI::log( '' );
|
|
\WP_CLI::log( 'Settings:' );
|
|
foreach ( Settings::all() as $k => $v ) {
|
|
if ( is_array( $v ) ) {
|
|
$v = '[' . implode( ',', $v ) . ']';
|
|
} elseif ( is_bool( $v ) ) {
|
|
$v = $v ? 'true' : 'false';
|
|
}
|
|
\WP_CLI::log( sprintf( ' %-28s %s', $k, $v ) );
|
|
}
|
|
|
|
\WP_CLI::log( '' );
|
|
\WP_CLI::log( 'Originals dir: ' . Optimizer::originals_root() );
|
|
\WP_CLI::log( ' exists: ' . ( is_dir( Optimizer::originals_root() ) ? 'yes' : 'no' ) );
|
|
}
|
|
|
|
/**
|
|
* Optimise a single attachment by ID.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* --id=<id>
|
|
* : Attachment post ID.
|
|
*
|
|
* [--dry-run]
|
|
* : Report what would happen without writing.
|
|
*
|
|
* ## EXAMPLES
|
|
* wp h4b-img optimise --id=12345
|
|
* wp h4b-img optimise --id=12345 --dry-run
|
|
*/
|
|
public function optimise( $args, $assoc ): void {
|
|
$id = (int) ( $assoc['id'] ?? 0 );
|
|
if ( $id <= 0 ) {
|
|
\WP_CLI::error( '--id required' );
|
|
}
|
|
$dry_run = ! empty( $assoc['dry-run'] );
|
|
|
|
$metadata = wp_get_attachment_metadata( $id );
|
|
if ( ! $metadata ) {
|
|
\WP_CLI::error( "No metadata for attachment $id" );
|
|
}
|
|
$uploads = wp_get_upload_dir();
|
|
$basedir = trailingslashit( $uploads['basedir'] );
|
|
$relative = get_post_meta( $id, '_wp_attached_file', true );
|
|
if ( ! $relative ) {
|
|
\WP_CLI::error( "Attachment $id has no _wp_attached_file" );
|
|
}
|
|
$full_path = $basedir . $relative;
|
|
if ( ! is_readable( $full_path ) ) {
|
|
\WP_CLI::error( "Not readable: $full_path" );
|
|
}
|
|
|
|
\WP_CLI::log( "Attachment $id: $relative" );
|
|
\WP_CLI::log( ' full path: ' . $full_path );
|
|
|
|
$summary = [
|
|
'full' => self::run_one( $id, 'full', $full_path, $dry_run ),
|
|
];
|
|
|
|
$dir = dirname( $full_path );
|
|
foreach ( ( $metadata['sizes'] ?? [] ) as $size_key => $size_data ) {
|
|
if ( empty( $size_data['file'] ) ) {
|
|
continue;
|
|
}
|
|
$path = trailingslashit( $dir ) . $size_data['file'];
|
|
if ( ! is_readable( $path ) ) {
|
|
continue;
|
|
}
|
|
$summary[ $size_key ] = self::run_one( $id, (string) $size_key, $path, $dry_run );
|
|
}
|
|
|
|
// Tabular summary
|
|
$rows = [];
|
|
foreach ( $summary as $size => $r ) {
|
|
$rows[] = [
|
|
'size' => $size,
|
|
'status' => $r['status'] ?? '',
|
|
'before' => $r['before'] ?? 0,
|
|
'after' => $r['after'] ?? 0,
|
|
'pct' => $r['percent'] ?? 0,
|
|
'icc' => ! empty( $r['icc_preserved'] ) ? '✓' : '',
|
|
'webp' => $r['webp_size'] ?? '',
|
|
'avif' => $r['avif_status'] ?? '',
|
|
'error' => $r['error'] ?? '',
|
|
];
|
|
}
|
|
\WP_CLI\Utils\format_items( 'table', $rows, [ 'size', 'status', 'before', 'after', 'pct', 'icc', 'webp', 'avif', 'error' ] );
|
|
}
|
|
|
|
private static function run_one( int $id, string $size_key, string $path, bool $dry_run ): array {
|
|
if ( $dry_run ) {
|
|
return [
|
|
'status' => 'dry-run',
|
|
'before' => filesize( $path ),
|
|
'after' => filesize( $path ),
|
|
'percent' => 0,
|
|
];
|
|
}
|
|
$opt = Optimizer::optimise( $path );
|
|
|
|
$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 );
|
|
}
|
|
|
|
$record = [
|
|
'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,
|
|
];
|
|
Attachment_Meta::record_size( $id, $size_key, $record );
|
|
|
|
return [
|
|
'status' => $opt['status'],
|
|
'before' => $opt['before'],
|
|
'after' => $opt['after'],
|
|
'percent' => $opt['percent'],
|
|
'icc_preserved' => $opt['icc_preserved'] ?? false,
|
|
'webp_size' => $webp_stats['status'] === 'done' ? $webp_stats['size'] : '',
|
|
'avif_status' => $avif_stats['status'],
|
|
'error' => $opt['error'] ?? null,
|
|
];
|
|
}
|
|
}
|