fix: eliminate AVIF/WebP grey halo on white backgrounds (v0.2.3)

Diagnosed visible faint grey rectangle around B&W ink art images on
pages with pure-white backgrounds. Root cause was format encoder
luminance shift in near-white pixels:

  Source JPG corner: (253,253,253) = #FDFDFD = 99.2% white
  AVIF q=65 (default): (250,250,250) = #FAFAFA = 98.0% (1.2% halo)
  WebP q=80 (default): (249,249,249) = #F9F9F9 = 97.6% (1.6% halo)

Two changes:

1. avifenc now uses -y 444 (full chroma subsampling) instead of
   default 4:2:0. Brings AVIF corner to #FBFBFB = 98.4%, smaller
   file size as a bonus (~10% reduction on a typical art image).

2. WebP default quality raised 80 → 90. Reaches #FDFDFD = exact
   match with source JPG. File size increases ~30% but eliminates
   the halo entirely for WebP-capable browsers (vast majority).

AVIF still has 0.4% residual halo (libavif 0.11.1 ceiling at this
quality range — pushing higher yields no improvement, only file
size). Acceptable tradeoff: WebP is the served-by-default fallback
when AVIF isn't perfect.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Henk
2026-05-19 15:31:53 +10:00
parent c54eccc2d5
commit 9a018e0562
3 changed files with 10 additions and 4 deletions

View File

@@ -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.2 * 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.2' ); 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__ ) );

View File

@@ -229,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,

View File

@@ -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