Skip to content

PNG transparency lost when GD engine generates WebP variants (imSaveWebP missing alpha flags) #2250

@elabx

Description

@elabx

Note on authorship: Drafted with Claude (Anthropic) after tracing the WebP code path in ImageSizerEngineGD. I have not yet patched and re-tested on my own install — flagging this so it can be evaluated/repro'd properly before merging. Cross-referenced against the canonical PHP-GD answer at https://stackoverflow.com/questions/67587861.

Affected versions: Observed against 3.0.262. The imSaveWebP() method has been structurally unchanged for several versions, so this likely affects every release that supports the webpAdd / webpOnly image options when the GD engine is in use.

Affected engine: ImageSizerEngineGD only. IMagick engine handles alpha correctly on its own.

Symptom

PNG source images with transparency get their alpha channel flattened to black (or to whatever background color the GD resource happens to carry) when the WebP variant is generated via image options like ["webpAdd" => true] or ["webpOnly" => true]. The original PNG output keeps transparency; only the sibling .webp file is broken.

Code location

wire/core/Image/ImageSizerEngineGD.php, lines 456–462:

protected function imSaveWebP($im, $filename, $quality = 90) {
    if(!function_exists('imagewebp')) return false;
    $path_parts = pathinfo($filename);
    $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp';
    if(file_exists($webpFilename)) $this->wire()->files->unlink($webpFilename);
    return imagewebp($im, $webpFilename, $quality);
}

Root cause

imagewebp() in PHP-GD requires the source resource to (a) be true-color and (b) have imagesavealpha(true) set at save time, otherwise the alpha channel is discarded. imSaveWebP() calls imagewebp() directly without ensuring either.

prepareImageLayer() (line 855) does set the right flags for PNG sources on the initial destination resource:

if($this->imageType == IMAGETYPE_PNG) {
    imagealphablending($im, false);
    imagesavealpha($im, true);
}

…but several downstream operations in this engine create new GD resources that don't inherit those flags. imRotate() (imagerotate), imFlip() (imagecreatetruecolor + imagecopyresampled), and sharpen/convolution paths can all hand imSaveWebP() a resource where savealpha is no longer true. When the original PNG passes through any of those transforms, the WebP variant comes out without transparency.

Proposed fix

Defensive flag-setting inside imSaveWebP() itself — covers every code path that lands here, regardless of upstream transforms:

protected function imSaveWebP($im, $filename, $quality = 90) {
    if(!function_exists('imagewebp')) return false;
    $path_parts = pathinfo($filename);
    $webpFilename = $path_parts['dirname'] . '/' . $path_parts['filename'] . '.webp';
    if(file_exists($webpFilename)) $this->wire()->files->unlink($webpFilename);

    // Ensure the resource is in a state imagewebp() can preserve alpha from.
    // imagewebp() silently drops the alpha channel if these aren't set, which
    // is the cause of PNG-transparency loss on WebP variants.
    if(function_exists('imagepalettetotruecolor')) imagepalettetotruecolor($im);
    imagealphablending($im, false);
    imagesavealpha($im, true);

    return imagewebp($im, $webpFilename, $quality);
}

This mirrors the canonical PHP-GD pattern for transparency-safe WebP writes and matches what imSavePNG-style paths already rely on for PNG output.

Note: imSaveWebP() is not currently hookable (no ___ prefix), so users cannot work around this from userland without either editing core or switching to the IMagick engine.

Not verified yet

I have not applied this patch on my own install and re-rendered images to confirm the WebP now has alpha — happy to do so if helpful, but wanted to file the analysis first so it isn't lost.

Environment

  • ProcessWire 3.0.262
  • PHP 7.4
  • GD engine (default)
  • PNG-24 source images with alpha channel

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions