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:
Henk
2026-05-19 13:41:03 +10:00
commit 7e1c86f215
19 changed files with 2498 additions and 0 deletions

68
includes/class-tools.php Normal file
View File

@@ -0,0 +1,68 @@
<?php
/**
* Detects + caches paths to the external image binaries.
*
* @package H4B\ImageOptim
*/
namespace H4B\ImageOptim;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class Tools {
private const REQUIRED = [ 'cwebp', 'cjpeg', 'djpeg', 'jpegtran', 'jpegoptim' ];
private const OPTIONAL = [ 'avifenc', 'avifdec', 'pngquant', 'optipng' ];
/**
* @return array<string,string|null> Map of tool name → absolute path or null.
*/
public static function detect(): array {
static $cache = null;
if ( null !== $cache ) {
return $cache;
}
$cache = [];
foreach ( array_merge( self::REQUIRED, self::OPTIONAL ) as $bin ) {
$cache[ $bin ] = self::which( $bin );
}
return $cache;
}
public static function path( string $bin ): ?string {
$tools = self::detect();
return $tools[ $bin ] ?? null;
}
public static function missing_required(): array {
$tools = self::detect();
$missing = [];
foreach ( self::REQUIRED as $bin ) {
if ( empty( $tools[ $bin ] ) ) {
$missing[] = $bin;
}
}
return $missing;
}
public static function has_avif(): bool {
return ! empty( self::path( 'avifenc' ) );
}
private static function which( string $bin ): ?string {
// Common locations first, then fall back to PATH lookup.
foreach ( [ '/usr/bin/', '/usr/local/bin/', '/opt/local/bin/' ] as $prefix ) {
$candidate = $prefix . $bin;
if ( is_executable( $candidate ) ) {
return $candidate;
}
}
if ( ! function_exists( 'shell_exec' ) ) {
return null;
}
$out = trim( (string) @shell_exec( 'command -v ' . escapeshellarg( $bin ) . ' 2>/dev/null' ) );
return $out !== '' && is_executable( $out ) ? $out : null;
}
}