Unblocks production use on sites previously running Smush.
migrate-from-smush:
- Reads wp-smpro-smush-data postmeta, writes _h4b_img_optim marker
- --dry-run / --force-rescan / --remove-smush-meta / --limit flags
- Verified: 100 attachments migrated cleanly on dev.rds.ink,
bulk count drops from 734 → 634
Picture_Tag rewriter:
- Hooks the_content + post_thumbnail_html + widget_text + Elementor
frontend + wp_get_attachment_image at priority 99
- Wraps <img> in <picture><source avif><source webp><img></picture>
when sibling files exist
- Double-wrap protection via byte-range tracking of existing <picture> blocks
- Per-image opt-out via data-no-h4b attribute
- Cached sibling lookups per request
- 8 edge-case tests pass
LOC: 2480 (was 1997). Adds class-cli-migrate.php (193 LOC) and
class-picture-tag.php (284 LOC).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
194 lines
5.9 KiB
PHP
194 lines
5.9 KiB
PHP
<?php
|
|
/**
|
|
* wp h4b-img migrate-from-smush
|
|
*
|
|
* Reads Smush's `wp-smpro-smush-data` postmeta and writes an equivalent
|
|
* `_h4b_img_optim` marker so our bulk command skips already-processed images.
|
|
*
|
|
* @package H4B\ImageOptim
|
|
*/
|
|
|
|
namespace H4B\ImageOptim;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
final class CLI_Migrate {
|
|
|
|
public const SMUSH_META_KEY = 'wp-smpro-smush-data';
|
|
|
|
/**
|
|
* Migrate Smush optimisation history into h4b-image-optim's tracking.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* [--dry-run]
|
|
* : Count only — do not write any postmeta.
|
|
*
|
|
* [--force-rescan]
|
|
* : Mark Smush-processed images as UNPROCESSED so our bulk command
|
|
* re-runs them through our pipeline. Use this when migrating off
|
|
* Smush and you want to re-optimise everything with our ICC-safe
|
|
* settings. Default: false (skip them, trust Smush did fine).
|
|
*
|
|
* [--remove-smush-meta]
|
|
* : After migration, delete the wp-smpro-smush-data postmeta. Frees
|
|
* ~1KB per attachment. Default: false (keep for audit).
|
|
*
|
|
* [--limit=<n>]
|
|
* : Max attachments to migrate. 0 = unlimited.
|
|
*
|
|
* ## EXAMPLES
|
|
* # See how many Smush-processed images exist
|
|
* wp h4b-img migrate-from-smush --dry-run
|
|
*
|
|
* # Trust Smush's prior work — just mark them processed
|
|
* wp h4b-img migrate-from-smush
|
|
*
|
|
* # Re-optimise everything from scratch through our pipeline
|
|
* wp h4b-img migrate-from-smush --force-rescan
|
|
*
|
|
* # Also strip the Smush postmeta after migration
|
|
* wp h4b-img migrate-from-smush --remove-smush-meta
|
|
*/
|
|
public function __invoke( $args, $assoc ): void {
|
|
$dry_run = ! empty( $assoc['dry-run'] );
|
|
$force_rescan = ! empty( $assoc['force-rescan'] );
|
|
$remove_smush = ! empty( $assoc['remove-smush-meta'] );
|
|
$limit = (int) ( $assoc['limit'] ?? 0 );
|
|
|
|
global $wpdb;
|
|
|
|
$base_sql = "SELECT pm.post_id, pm.meta_value, p.post_mime_type
|
|
FROM {$wpdb->postmeta} pm
|
|
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
|
WHERE pm.meta_key = %s
|
|
ORDER BY pm.post_id ASC";
|
|
if ( $limit > 0 ) {
|
|
$base_sql .= ' LIMIT ' . (int) $limit;
|
|
}
|
|
$rows = $wpdb->get_results( $wpdb->prepare( $base_sql, self::SMUSH_META_KEY ) );
|
|
|
|
$total = count( $rows );
|
|
\WP_CLI::log( "Found $total attachments with Smush optimisation history." );
|
|
if ( $total === 0 ) {
|
|
\WP_CLI::success( 'Nothing to migrate.' );
|
|
return;
|
|
}
|
|
|
|
if ( $dry_run ) {
|
|
$action = $force_rescan ? 'mark for re-optimisation' : 'mark as already processed';
|
|
$remove = $remove_smush ? ' + remove Smush meta' : '';
|
|
\WP_CLI::success( "Dry-run only. Would $action $total attachments$remove." );
|
|
return;
|
|
}
|
|
|
|
$progress = \WP_CLI\Utils\make_progress_bar( 'Migrating', $total );
|
|
$migrated = 0;
|
|
$skipped = 0;
|
|
$errored = 0;
|
|
$removed_meta = 0;
|
|
|
|
foreach ( $rows as $row ) {
|
|
$id = (int) $row->post_id;
|
|
try {
|
|
if ( $force_rescan ) {
|
|
// Wipe our marker so bulk treats this as unprocessed
|
|
delete_post_meta( $id, Attachment_Meta::META_KEY );
|
|
} else {
|
|
$existing = Attachment_Meta::get( $id );
|
|
if ( ! empty( $existing['processed_at'] ) ) {
|
|
$skipped++;
|
|
$progress->tick();
|
|
continue;
|
|
}
|
|
$stats = self::translate_smush_stats( (string) $row->meta_value );
|
|
$meta = [
|
|
'version' => H4B_IMG_OPTIM_VERSION,
|
|
'processed_at' => current_time( 'c' ),
|
|
'migrated_from' => 'smush',
|
|
'tool_chain' => [ 'smush_legacy' ],
|
|
'sizes' => $stats['sizes'],
|
|
'totals' => $stats['totals'],
|
|
'note' => 'Migrated from Smush — not re-processed by h4b-image-optim.',
|
|
];
|
|
Attachment_Meta::set( $id, $meta );
|
|
}
|
|
|
|
if ( $remove_smush ) {
|
|
if ( delete_post_meta( $id, self::SMUSH_META_KEY ) ) {
|
|
$removed_meta++;
|
|
}
|
|
}
|
|
$migrated++;
|
|
} catch ( \Throwable $e ) {
|
|
$errored++;
|
|
}
|
|
$progress->tick();
|
|
}
|
|
$progress->finish();
|
|
|
|
\WP_CLI::success( sprintf(
|
|
"Migration done.\n Migrated: %d\n Already-marked (skipped): %d\n Errors: %d\n Smush meta removed: %d",
|
|
$migrated, $skipped, $errored, $removed_meta
|
|
) );
|
|
}
|
|
|
|
/**
|
|
* Translate Smush's serialized stats into our schema.
|
|
*
|
|
* Smush stores:
|
|
* a:2:{
|
|
* s:"stats" => [ time, bytes, percent, size_before, size_after, lossy, keep_exif, api_version ],
|
|
* s:"sizes" => [ <size_key> => stdClass{ time, bytes, percent, size_before, size_after }, ... ]
|
|
* }
|
|
*/
|
|
private static function translate_smush_stats( string $raw ): array {
|
|
$result = [
|
|
'sizes' => [],
|
|
'totals' => [
|
|
'bytes_saved' => 0,
|
|
'bytes_before' => 0,
|
|
'bytes_after' => 0,
|
|
'percent' => 0,
|
|
],
|
|
];
|
|
|
|
// Smush uses PHP serialize. Allow stdClass.
|
|
$data = @unserialize( $raw, [ 'allowed_classes' => [ \stdClass::class ] ] );
|
|
if ( ! is_array( $data ) ) {
|
|
return $result;
|
|
}
|
|
|
|
if ( isset( $data['stats'] ) && is_array( $data['stats'] ) ) {
|
|
$result['totals']['bytes_saved'] = (int) ( $data['stats']['bytes'] ?? 0 );
|
|
$result['totals']['bytes_before'] = (int) ( $data['stats']['size_before'] ?? 0 );
|
|
$result['totals']['bytes_after'] = (int) ( $data['stats']['size_after'] ?? 0 );
|
|
$result['totals']['percent'] = (float) ( $data['stats']['percent'] ?? 0 );
|
|
}
|
|
|
|
if ( isset( $data['sizes'] ) && is_array( $data['sizes'] ) ) {
|
|
foreach ( $data['sizes'] as $size_key => $info ) {
|
|
// $info can be stdClass or array
|
|
$arr = is_object( $info ) ? get_object_vars( $info ) : (array) $info;
|
|
$result['sizes'][ (string) $size_key ] = [
|
|
'status' => 'migrated_from_smush',
|
|
'before' => (int) ( $arr['size_before'] ?? 0 ),
|
|
'after' => (int) ( $arr['size_after'] ?? 0 ),
|
|
'percent' => (float) ( $arr['percent'] ?? 0 ),
|
|
'icc_preserved' => false, // unknown — Smush stripped it
|
|
'tool_chain' => [ 'smush_legacy' ],
|
|
'webp' => null,
|
|
'avif' => null,
|
|
'avif_status' => 'never_generated',
|
|
'backup' => null,
|
|
'error' => null,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|