Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.33% covered (success)
93.33%
56 / 60
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
PngParser
93.33% covered (success)
93.33%
56 / 60
50.00% covered (danger)
50.00%
1 / 2
25.19
0.00% covered (danger)
0.00%
0 / 1
 parseFile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 parse
92.86% covered (success)
92.86%
52 / 56
0.00% covered (danger)
0.00%
0 / 1
24.21
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\ImageMetadata;
6
7use Phpdftk\Filesystem\LocalFilesystem;
8
9/**
10 * Parse PNG IHDR chunk for dimensions, bit depth, and color type.
11 *
12 * Also extracts ICC profiles from iCCP chunks. PNG alpha channels
13 * are detected since PDF handles transparency via SMask, not inline.
14 */
15final class PngParser
16{
17    public static function parseFile(string $path): ImageInfo
18    {
19        $fh = LocalFilesystem::openReadable($path, "image file");
20        try {
21            $data = fread($fh, filesize($path));
22        } finally {
23            fclose($fh);
24        }
25        return self::parse($data);
26    }
27
28    public static function parse(string $data): ImageInfo
29    {
30        $len = strlen($data);
31
32        // Verify PNG signature (8 bytes)
33        if ($len < 8 || substr($data, 0, 8) !== "\x89PNG\r\n\x1A\n") {
34            throw new \RuntimeException('Not a valid PNG file');
35        }
36
37        $pos = 8;
38        $width = 0;
39        $height = 0;
40        $bitDepth = 8;
41        $colorType = 2;
42        $xDpi = null;
43        $yDpi = null;
44        $iccProfile = null;
45
46        while ($pos + 12 <= $len) {
47            $chunkLen  = unpack('N', substr($data, $pos, 4))[1];
48            $chunkType = substr($data, $pos + 4, 4);
49            $chunkData = substr($data, $pos + 8, $chunkLen);
50            $pos += 12 + $chunkLen;
51
52            if ($chunkType === 'IHDR' && strlen($chunkData) >= 13) {
53                $width    = unpack('N', substr($chunkData, 0, 4))[1];
54                $height   = unpack('N', substr($chunkData, 4, 4))[1];
55                $bitDepth = ord($chunkData[8]);
56                $colorType = ord($chunkData[9]);
57            } elseif ($chunkType === 'pHYs' && strlen($chunkData) >= 9) {
58                $xPixelsPerUnit = unpack('N', substr($chunkData, 0, 4))[1];
59                $yPixelsPerUnit = unpack('N', substr($chunkData, 4, 4))[1];
60                $unit = ord($chunkData[8]);
61                if ($unit === 1 && $xPixelsPerUnit > 0 && $yPixelsPerUnit > 0) {
62                    // Unit is meters; convert to DPI
63                    $xDpi = (int) round($xPixelsPerUnit / 39.3701);
64                    $yDpi = (int) round($yPixelsPerUnit / 39.3701);
65                }
66            } elseif ($chunkType === 'iCCP' && strlen($chunkData) > 2) {
67                // iCCP chunk: null-terminated profile name, 1-byte compression method, compressed data
68                $nullPos = strpos($chunkData, "\x00");
69                if ($nullPos !== false && $nullPos + 2 <= strlen($chunkData)) {
70                    // Skip profile name + null byte + compression method byte (always 0 = deflate)
71                    $compressedData = substr($chunkData, $nullPos + 2);
72                    if ($compressedData !== '') {
73                        $decompressed = @gzuncompress($compressedData);
74                        if ($decompressed !== false) {
75                            $iccProfile = $decompressed;
76                        }
77                    }
78                }
79            } elseif ($chunkType === 'IEND') {
80                break;
81            }
82        }
83
84        [$colorSpace, $hasAlpha] = match ($colorType) {
85            0 => ['DeviceGray', false],
86            2 => ['DeviceRGB', false],
87            3 => ['DeviceRGB', false],  // indexed â€” treat as RGB
88            4 => ['DeviceGray', true],
89            6 => ['DeviceRGB', true],
90            default => ['DeviceRGB', false],
91        };
92
93        return new ImageInfo(
94            width: $width,
95            height: $height,
96            colorSpace: $colorSpace,
97            bitsPerComponent: $bitDepth,
98            format: 'png',
99            hasAlpha: $hasAlpha,
100            xDpi: $xDpi,
101            yDpi: $yDpi,
102            iccProfile: $iccProfile,
103        );
104    }
105}