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

View File

@@ -0,0 +1,67 @@
<?php
/**
* ICC profile handling — the critical fix for the Smush grey-wash bug.
*
* If an image has an ICC profile, KEEP IT. Stripping ICC + applying YCbCr
* subsampling is what produced the grey-wash on rds.ink art.
*
* If an image has NO profile, the safest default is to embed a small sRGB v4
* profile so browsers don't fall back to vendor-specific assumptions.
*
* @package H4B\ImageOptim
*/
namespace H4B\ImageOptim;
use Imagick;
use ImagickException;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class ICC_Profile {
/**
* Path to the bundled sRGB v4 ICC profile.
* If missing, we silently skip injection (the file's pixels will just
* be interpreted as implicit sRGB by the browser).
*/
public static function srgb_profile_path(): string {
return H4B_IMG_OPTIM_DIR . 'assets/sRGB_v4_ICC_preference.icc';
}
/**
* Inspect an Imagick image and ensure an ICC profile is present.
* Does NOT modify the colour space — only attaches a profile.
*
* @return array{result:bool, source:string} result is true if a profile
* is present/added. source is one of: 'original','injected','none'.
*/
public static function preserve_or_inject( Imagick $img ): array {
try {
$profiles = $img->getImageProfiles( 'icc', true );
} catch ( ImagickException $e ) {
$profiles = [];
}
if ( ! empty( $profiles ) ) {
// Source has an ICC profile — leave it alone. This is the
// common case for camera JPEGs and properly-saved Photoshop exports.
return [ 'result' => true, 'source' => 'original' ];
}
// No profile. Try to inject sRGB v4 fallback.
$profile_path = self::srgb_profile_path();
if ( ! is_readable( $profile_path ) ) {
return [ 'result' => false, 'source' => 'none' ];
}
try {
$img->setImageColorspace( Imagick::COLORSPACE_SRGB );
$img->profileImage( 'icc', (string) file_get_contents( $profile_path ) );
return [ 'result' => true, 'source' => 'injected' ];
} catch ( ImagickException $e ) {
return [ 'result' => false, 'source' => 'none' ];
}
}
}