Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
WebpParser
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
3 / 3
22
100.00% covered (success)
100.00%
1 / 1
 parseFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
1 / 1
16
 findChunk
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\ImageMetadata;
6
7use Phpdftk\Filesystem\LocalFilesystem;
8
9/**
10 * Parse WebP container (RIFF/VP8/VP8L/VP8X) for dimensions and alpha.
11 */
12final class WebpParser
13{
14    public static function parseFile(string $path): ImageInfo
15    {
16        $data = LocalFilesystem::readFile($path, "image file");
17        return self::parse($data);
18    }
19
20    public static function parse(string $data): ImageInfo
21    {
22        $len = strlen($data);
23        if ($len < 12) {
24            throw new \RuntimeException('Not enough data for WebP');
25        }
26
27        if (substr($data, 0, 4) !== 'RIFF' || substr($data, 8, 4) !== 'WEBP') {
28            throw new \RuntimeException('Not a valid WebP file');
29        }
30
31        if ($len < 16) {
32            throw new \RuntimeException('WebP data too short');
33        }
34
35        $chunkType = substr($data, 12, 4);
36        $width = 0;
37        $height = 0;
38        $iccProfile = null;
39        $hasAlpha = false;
40
41        if ($chunkType === 'VP8 ') {
42            // Lossy VP8
43            if ($len < 30) {
44                throw new \RuntimeException('WebP VP8 data too short');
45            }
46            $offset = 23;
47            if (ord($data[$offset]) === 0x9D && ord($data[$offset + 1]) === 0x01 && ord($data[$offset + 2]) === 0x2A) {
48                $w = unpack('v', substr($data, $offset + 3, 2))[1];
49                $h = unpack('v', substr($data, $offset + 5, 2))[1];
50                $width  = $w & 0x3FFF;
51                $height = $h & 0x3FFF;
52            }
53        } elseif ($chunkType === 'VP8L') {
54            // Lossless VP8L
55            if ($len < 25) {
56                throw new \RuntimeException('WebP VP8L data too short');
57            }
58            $offset = 20;
59            if (ord($data[$offset]) === 0x2F) {
60                $packed = unpack('V', substr($data, $offset + 1, 4))[1];
61                $width  = ($packed & 0x3FFF) + 1;
62                $height = (($packed >> 14) & 0x3FFF) + 1;
63            }
64        } elseif ($chunkType === 'VP8X') {
65            // Extended VP8X
66            if ($len < 30) {
67                throw new \RuntimeException('WebP VP8X data too short');
68            }
69
70            // Flags byte at offset 20
71            $flags = ord($data[20]);
72            $hasIcc = ($flags & 0x20) !== 0;   // bit 5: ICC profile
73            $hasAlpha = ($flags & 0x10) !== 0;  // bit 4: alpha
74
75            // Canvas dimensions at offset 24
76            $wBytes = substr($data, 24, 3) . "\x00";
77            $hBytes = substr($data, 27, 3) . "\x00";
78            $width  = unpack('V', $wBytes)[1] + 1;
79            $height = unpack('V', $hBytes)[1] + 1;
80
81            // Scan for ICCP chunk if flag indicates ICC profile is present
82            if ($hasIcc) {
83                $iccProfile = self::findChunk($data, $len, 'ICCP');
84            }
85        }
86
87        return new ImageInfo(
88            width: $width,
89            height: $height,
90            colorSpace: 'DeviceRGB',
91            bitsPerComponent: 8,
92            format: 'webp',
93            hasAlpha: $hasAlpha,
94            iccProfile: $iccProfile,
95        );
96    }
97
98    /**
99     * Search for a named chunk in the WebP RIFF container.
100     * Chunks start at offset 12 (after RIFF + filesize + WEBP).
101     */
102    private static function findChunk(string $data, int $len, string $chunkId): ?string
103    {
104        $pos = 12; // after 'RIFF' + 4-byte size + 'WEBP'
105
106        while ($pos + 8 <= $len) {
107            $tag = substr($data, $pos, 4);
108            $chunkSize = unpack('V', $data, $pos + 4)[1];
109
110            if ($tag === $chunkId) {
111                $dataStart = $pos + 8;
112                if ($dataStart + $chunkSize <= $len) {
113                    return substr($data, $dataStart, $chunkSize);
114                }
115                return null;
116            }
117
118            // Advance to next chunk (chunks are 2-byte aligned)
119            $pos += 8 + $chunkSize;
120            if ($chunkSize % 2 !== 0) {
121                $pos++; // padding byte
122            }
123        }
124
125        return null;
126    }
127}