Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e56ca6e63 | ||
|
|
c54eccc2d5 |
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: H4B Image Optim
|
* Plugin Name: H4B Image Optim
|
||||||
* Plugin URI: https://gitea.help4bis.com/help4bis/h4b-image-optim
|
* Plugin URI: https://gitea.help4bis.com/help4bis/h4b-image-optim
|
||||||
* Description: ICC-safe image optimisation with WebP + AVIF generation. Replaces Smush Pro without the grey-wash bug. No CDN.
|
* Description: ICC-safe image optimisation with WebP + AVIF generation. Replaces Smush Pro without the grey-wash bug. No CDN.
|
||||||
* Version: 0.2.1
|
* Version: 0.2.3
|
||||||
* Author: help4bis (Henk + Claude)
|
* Author: help4bis (Henk + Claude)
|
||||||
* Author URI: https://help4bis.com
|
* Author URI: https://help4bis.com
|
||||||
* License: GPL-2.0-or-later
|
* License: GPL-2.0-or-later
|
||||||
@@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
define( 'H4B_IMG_OPTIM_VERSION', '0.2.1' );
|
define( 'H4B_IMG_OPTIM_VERSION', '0.2.3' );
|
||||||
define( 'H4B_IMG_OPTIM_FILE', __FILE__ );
|
define( 'H4B_IMG_OPTIM_FILE', __FILE__ );
|
||||||
define( 'H4B_IMG_OPTIM_DIR', plugin_dir_path( __FILE__ ) );
|
define( 'H4B_IMG_OPTIM_DIR', plugin_dir_path( __FILE__ ) );
|
||||||
define( 'H4B_IMG_OPTIM_URL', plugin_dir_url( __FILE__ ) );
|
define( 'H4B_IMG_OPTIM_URL', plugin_dir_url( __FILE__ ) );
|
||||||
|
|||||||
@@ -143,6 +143,104 @@ final class CLI_Siblings {
|
|||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconcile attachment postmeta with what's actually on disk.
|
||||||
|
*
|
||||||
|
* For each `_h4b_img_optim` size entry, if avif_status is 'queued' or 'never_generated'
|
||||||
|
* but the .avif file exists on disk, update to 'done' with the actual byte size.
|
||||||
|
* Similarly for webp.
|
||||||
|
*
|
||||||
|
* Use this after manual generate-missing-siblings runs OR to fix the stale
|
||||||
|
* 'queued' values from a v0.2.0 era bulk run.
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* [--dry-run]
|
||||||
|
* : Report counts only.
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
* wp h4b-img reconcile-meta --dry-run
|
||||||
|
* wp h4b-img reconcile-meta
|
||||||
|
*/
|
||||||
|
public function reconcile_meta( $args, $assoc ): void {
|
||||||
|
$dry_run = ! empty( $assoc['dry-run'] );
|
||||||
|
global $wpdb;
|
||||||
|
$basedir = trailingslashit( wp_get_upload_dir()['basedir'] );
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key='_h4b_img_optim'" );
|
||||||
|
\WP_CLI::log( sprintf( 'Scanning %d attachments…', count( $rows ) ) );
|
||||||
|
|
||||||
|
$updates = [
|
||||||
|
'avif_done_marked' => 0,
|
||||||
|
'avif_already_done' => 0,
|
||||||
|
'avif_still_missing' => 0,
|
||||||
|
'webp_done_marked' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ( $rows as $r ) {
|
||||||
|
$meta = @unserialize( $r->meta_value, [ 'allowed_classes' => false ] );
|
||||||
|
if ( ! is_array( $meta ) || empty( $meta['sizes'] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$rel_parent = get_post_meta( $r->post_id, '_wp_attached_file', true );
|
||||||
|
if ( ! $rel_parent ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parent_dir = dirname( $rel_parent );
|
||||||
|
$attached_meta = wp_get_attachment_metadata( $r->post_id );
|
||||||
|
$changed = false;
|
||||||
|
foreach ( $meta['sizes'] as $size_key => &$entry ) {
|
||||||
|
// Resolve the file path for this size key
|
||||||
|
if ( $size_key === 'full' ) {
|
||||||
|
$file_rel = $rel_parent;
|
||||||
|
} elseif ( is_array( $attached_meta ) && ! empty( $attached_meta['sizes'][ $size_key ]['file'] ) ) {
|
||||||
|
$f = $attached_meta['sizes'][ $size_key ]['file'];
|
||||||
|
$file_rel = ( $parent_dir === '.' ) ? $f : "$parent_dir/$f";
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$src = $basedir . $file_rel;
|
||||||
|
$avif = $src . '.avif';
|
||||||
|
$webp = $src . '.webp';
|
||||||
|
|
||||||
|
// AVIF
|
||||||
|
if ( is_readable( $avif ) ) {
|
||||||
|
if ( ( $entry['avif_status'] ?? '' ) !== 'done' ) {
|
||||||
|
$entry['avif'] = filesize( $avif );
|
||||||
|
$entry['avif_status'] = 'done';
|
||||||
|
$updates['avif_done_marked']++;
|
||||||
|
$changed = true;
|
||||||
|
} else {
|
||||||
|
$updates['avif_already_done']++;
|
||||||
|
}
|
||||||
|
} elseif ( ( $entry['avif_status'] ?? '' ) === 'queued' ) {
|
||||||
|
$updates['avif_still_missing']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebP — only mark if currently null/missing
|
||||||
|
if ( is_readable( $webp ) && empty( $entry['webp'] ) ) {
|
||||||
|
$entry['webp'] = filesize( $webp );
|
||||||
|
$updates['webp_done_marked']++;
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset( $entry );
|
||||||
|
|
||||||
|
if ( $changed && ! $dry_run ) {
|
||||||
|
Attachment_Meta::set( (int) $r->post_id, $meta );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\WP_CLI::success( sprintf(
|
||||||
|
"Reconciliation %s.\n avif_status 'queued' → 'done' on disk: %d\n avif already correctly 'done': %d\n avif still missing (queue stale): %d\n webp filesize backfilled in meta: %d",
|
||||||
|
$dry_run ? 'dry-run' : 'done',
|
||||||
|
$updates['avif_done_marked'],
|
||||||
|
$updates['avif_already_done'],
|
||||||
|
$updates['avif_still_missing'],
|
||||||
|
$updates['webp_done_marked']
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the set of all JPG/PNG paths registered in wp_attachment_metadata.
|
* Build the set of all JPG/PNG paths registered in wp_attachment_metadata.
|
||||||
* Returns rel-path => true.
|
* Returns rel-path => true.
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ final class CLI {
|
|||||||
\WP_CLI::add_command( 'h4b-img rescue', CLI_Rescue::class );
|
\WP_CLI::add_command( 'h4b-img rescue', CLI_Rescue::class );
|
||||||
\WP_CLI::add_command( 'h4b-img migrate-from-smush', CLI_Migrate::class );
|
\WP_CLI::add_command( 'h4b-img migrate-from-smush', CLI_Migrate::class );
|
||||||
\WP_CLI::add_command( 'h4b-img generate-missing-siblings', CLI_Siblings::class );
|
\WP_CLI::add_command( 'h4b-img generate-missing-siblings', CLI_Siblings::class );
|
||||||
|
\WP_CLI::add_command( 'h4b-img reconcile-meta',
|
||||||
|
function ( $args, $assoc ) {
|
||||||
|
( new CLI_Siblings() )->reconcile_meta( $args, $assoc );
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -115,11 +115,15 @@ final class Format_Generator {
|
|||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::encode_avif_now( $source, $settings );
|
// Synchronous path: encode + record outcome in attachment postmeta.
|
||||||
|
$result = self::encode_avif_now( $source, $settings );
|
||||||
|
self::record_avif_outcome( $source, $result );
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cron entry point — encodes one queued path and updates per-attachment meta.
|
* Cron entry point — encodes one queued path. Recording happens inside
|
||||||
|
* make_avif() now, so this is a thin wrapper that forces synchronous mode.
|
||||||
*/
|
*/
|
||||||
public static function process_avif_job( string $source ): void {
|
public static function process_avif_job( string $source ): void {
|
||||||
if ( ! is_readable( $source ) ) {
|
if ( ! is_readable( $source ) ) {
|
||||||
@@ -127,8 +131,8 @@ final class Format_Generator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$settings = Settings::all();
|
$settings = Settings::all();
|
||||||
$result = self::encode_avif_now( $source, $settings );
|
$settings['avif_async'] = false; // we ARE the queue handler — never re-queue
|
||||||
self::record_avif_outcome( $source, $result );
|
self::make_avif( $source, $settings );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,8 +229,14 @@ final class Format_Generator {
|
|||||||
$dest = $source . '.avif';
|
$dest = $source . '.avif';
|
||||||
$tmp = $dest . '.h4b.tmp';
|
$tmp = $dest . '.h4b.tmp';
|
||||||
|
|
||||||
|
// --min / --max set quality range.
|
||||||
|
// -s sets encoder speed (0-10; we default to 6 = balanced).
|
||||||
|
// -y 444 forces 4:4:4 chroma subsampling — preserves luminance of near-white
|
||||||
|
// pixels exactly, otherwise avifenc rounds white-ish pixels darker (~2% shift)
|
||||||
|
// which creates a visible grey halo against pure-white page backgrounds.
|
||||||
|
// Tradeoff: ~5% larger AVIF files in exchange for true colour fidelity.
|
||||||
$cmd = sprintf(
|
$cmd = sprintf(
|
||||||
'%s --min %d --max %d -s %d %s %s 2>&1',
|
'%s --min %d --max %d -s %d -y 444 %s %s 2>&1',
|
||||||
escapeshellcmd( $bin ),
|
escapeshellcmd( $bin ),
|
||||||
$qmin,
|
$qmin,
|
||||||
$qmax,
|
$qmax,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ final class Settings {
|
|||||||
// Format generation
|
// Format generation
|
||||||
'generate_webp' => true,
|
'generate_webp' => true,
|
||||||
'generate_avif' => true, // design decision 1: on by default everywhere
|
'generate_avif' => true, // design decision 1: on by default everywhere
|
||||||
'webp_quality' => 80,
|
'webp_quality' => 90, // matches source JPG luminance for B&W ink on white
|
||||||
'avif_quality' => 65, // ≈ JPEG q=85 visually
|
'avif_quality' => 65, // ≈ JPEG q=85 visually
|
||||||
'avif_speed' => 6, // 0-10; 6 is balanced
|
'avif_speed' => 6, // 0-10; 6 is balanced
|
||||||
'avif_async' => true, // background WP-Cron so upload UI is responsive
|
'avif_async' => true, // background WP-Cron so upload UI is responsive
|
||||||
|
|||||||
Reference in New Issue
Block a user