Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect if a WEBP image is transparent, in PHP

Tags:

php

I'm trying to check if a webp image is transparent in PHP or not. Is it possible?

Best would be in pure php.

Update:

After researching... i wrote this php function. Works great.

webp_info() detect transparent and animation in a webp image.

function webp_info($f) {
    // https://github.com/webmproject/libwebp/blob/master/src/dec/webp_dec.c
    // https://developers.google.com/speed/webp/docs/riff_container
    // https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
    // https://stackoverflow.com/questions/61221874/detect-if-a-webp-image-is-transparent-in-php

    $fp = fopen($f, 'rb');
    if (!$fp) {
        throw new Exception("webp_info(): fopen($f): Failed");
    }
    $buf = fread($fp, 25);
    fclose($fp);

    switch (true) {
        case!is_string($buf):
        case strlen($buf) < 25:
        case substr($buf, 0, 4) != 'RIFF':
        case substr($buf, 8, 4) != 'WEBP':
        case substr($buf, 12, 3) != 'VP8':
            throw new Exception("webp_info(): not a valid webp image");

        case $buf[15] == ' ':
            // Simple File Format (Lossy)
            return array(
                'type'            => 'VP8',
                'has-animation'   => false,
                'has-transparent' => false,
            );


        case $buf[15] == 'L':
            // Simple File Format (Lossless)
            return array(
                'type'            => 'VP8L',
                'has-animation'   => false,
                'has-transparent' => (bool) (!!(ord($buf[24]) & 0x00000010)),
            );
        case $buf[15] == 'X':
            // Extended File Format
            return array(
                'type'            => 'VP8X',
                'has-animation'   => (bool) (!!(ord($buf[20]) & 0x00000002)),
                'has-transparent' => (bool) (!!(ord($buf[20]) & 0x00000010)),
            );

        default:
            throw new Exception("webp_info(): could not detect webp type");
    }
}

var_export(webp_info('image.webp'));
like image 982
Hans-Jürgen Petrich Avatar asked Sep 01 '25 04:09

Hans-Jürgen Petrich


1 Answers

Thank you @Hans-Jürgen Petrich for the idea.

I've update code to use unpack() which is easy to understand and then merge with the code from WordPress to get width and height of the image. Here is the result.

<?php


class WebP
{


    /**
     * @var string|null File path.
     */
    protected $file;


    /**
     * WebP file information class.
     *
     * @param string $file Path to WEBP file.
     */
    public function __construct($file = '')
    {
        if (is_string($file) && !empty($file)) {
            $this->file = $file;
        } else {
            $this->file = null;
        }
    }// __construct


    /**
     * Get WebP file info.
     * 
     * @link https://www.php.net/manual/en/function.pack.php unpack format reference.
     * @link https://developers.google.com/speed/webp/docs/riff_container WebP document.
     * @link https://stackoverflow.com/q/61221874/128761 Original question.
     * @param string $file Full path to image file. You can omit this argument and use one in class constructor instead.
     * @return array|false Return associative array if success, return `false` for otherwise.
     */
    public function webPInfo($file = '')
    {
        if (!is_string($file) || (is_string($file) && empty($file))) {
            $file = $this->file;
        }

        if (!is_file($file)) {
            // if file was not found.
            return false;
        } else {
            $file = realpath($file);
        }

        $fp = fopen($file, 'rb');
        if (!$fp) {
            // if could not open file.
            return false;
        }

        $data = fread($fp, 90);

        $header_format = 'A4RIFF/' . // get n string
            'I1FILESIZE/' . // get integer (file size but not actual size)
            'A4WEBP/' . // get n string
            'A4VP/' . // get n string
            'A74chunk';
        $header = unpack($header_format, $data);
        unset($header_format);

        // the conditions below means this file is not webp image.
        if (!isset($header['RIFF']) || strtoupper($header['RIFF']) !== 'RIFF') {
            return false;
        }
        if (!isset($header['WEBP']) || strtoupper($header['WEBP']) !== 'WEBP') {
            return false;
        }
        if (!isset($header['VP']) || strpos(strtoupper($header['VP']), 'VP8') === false) {
            return false;
        }

        // check for animation.
        if (
            strpos(strtoupper($header['chunk']), 'ANIM') !== false || 
            strpos(strtoupper($header['chunk']), 'ANMF') !== false
        ) {
            $header['ANIMATION'] = true;
        } else {
            $header['ANIMATION'] = false;
        }

        // check for transparent.
        if (strpos(strtoupper($header['chunk']), 'ALPH') !== false) {
            $header['ALPHA'] = true;
        } else {
            if (strpos(strtoupper($header['VP']), 'VP8L') !== false) {
                // if it is VP8L.
                // @link https://developers.google.com/speed/webp/docs/riff_container#simple_file_format_lossless Reference.
                $header['ALPHA'] = (bool) (!!(ord($data[24]) & 0x00000010));
            } elseif (strpos(strtoupper($header['VP']), 'VP8X') !== false) {
                // if it is VP8X.
                // @link https://developers.google.com/speed/webp/docs/riff_container#extended_file_format Reference.
                // @link https://stackoverflow.com/a/61242086/128761 Original source code.
                $header['ALPHA'] = (bool) (!!(ord($data[20]) & 0x00000010));
            } else {
                $header['ALPHA'] = false;
            }
        }

        // get width & height.
        // @link https://developer.wordpress.org/reference/functions/wp_get_webp_info/ Original source code.
        if (strtoupper($header['VP']) === 'VP8') {
            $parts = unpack('v2', substr($data, 26, 4));
            $header['WIDTH'] = (int) ($parts[1] & 0x3FFF);
            $header['HEIGHT'] = (int) ($parts[2] & 0x3FFF);
        } elseif (strtoupper($header['VP']) === 'VP8L') {
            $parts = unpack('C4', substr($data, 21, 4));
            $header['WIDTH'] = (int) (($parts[1] | (($parts[2] & 0x3F) << 8)) + 1);
            $header['HEIGHT'] = (int) (((($parts[2] & 0xC0) >> 6) | ($parts[3] << 2) | (($parts[4] & 0x03) << 10)) + 1);
        } elseif (strtoupper($header['VP']) === 'VP8X') {
            // Pad 24-bit int.
            $width = unpack('V', substr($data, 24, 3) . "\x00");
            $header['WIDTH'] = (int) ($width[1] & 0xFFFFFF) + 1;
            // Pad 24-bit int.
            $height = unpack('V', substr($data, 27, 3) . "\x00");
            $header['HEIGHT'] = (int) ($height[1] & 0xFFFFFF) + 1;
        }
        unset($height, $parts, $width);

        fclose($fp);
        unset($data, $fp, $header['chunk']);
        return $header;
    }// webPInfo


}

To use

$WebP = new WebP('/my/image.webp');
var_dump($WebP->webPInfo());

Next, the problem is... some animated pictures seems to have no any transparency part but the code detected as 'ALPHA' => true.
In fact, the transparency part is inside the another frame(s). It is also possible that some animated frames has different size. This is depend on what software export the image.

To check that what I said is true, use this code from Imagick to extract animated WEBP from each frames to files.

<?php

// change your settings here.
$sourceDir = 'img-src';
$fileName = 'animated4.webp';
$saveDir = 'processed';
// end change your settings.

$fileRelPath = $sourceDir . '/' . $fileName;

if (!is_dir($saveDir)) {
    mkdir($saveDir);
} else {
    $result = scandir($saveDir);
    foreach ($result as $file) {
        if ($file === '.' || $file === '..') {
            continue;
        }

        if (is_file($saveDir . '/' . $file)) {
            unlink($saveDir . '/' . $file);
        }
    }// endforeach;
    unset($file, $result);
}
?>
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <h1>Extract animated</h1>
        <p>Image to use.</p>
        <?php
        echo '<img src="' . $fileRelPath . '">';

        $Imagick = new \Imagick(realpath($fileRelPath));

        if (!is_object($Imagick)) {
            die('Failed to load image file.');
        }
        $Imagick->writeImages($saveDir . '/' . $fileName, false);

        unset($Imagick);

        echo '<h2>Extracted files</h2>' . "\n";
        $result = scandir($saveDir);
        foreach ($result as $file) {
            if ($file === '.' || $file === '..') {
                continue;
            }
    
            echo '<img src="' . $saveDir . '/' . $file . '">';
        }// endforeach;
        unset($file, $result);
        unset($fileName, $fileRelPath, $saveDir, $sourceDir);
        ?> 

    </body>
</html>
like image 195
vee Avatar answered Sep 02 '25 18:09

vee