Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
71 / 71
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
TiffParser
100.00% covered (success)
100.00%
71 / 71
100.00% covered (success)
100.00%
2 / 2
23
100.00% covered (success)
100.00%
1 / 1
 parseFile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
1 / 1
22
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\ImageMetadata;
6
7use Phpdftk\Filesystem\LocalFilesystem;
8
9/**
10 * Parse TIFF IFD tags for dimensions, color space, and bit depth.
11 *
12 * Handles both little-endian (II) and big-endian (MM) byte orders.
13 * TIFF is commonly encountered in scanned-document workflows.
14 */
15final class TiffParser
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        if ($len < 8) {
32            throw new \RuntimeException('Not enough data for TIFF');
33        }
34
35        $byteOrder = substr($data, 0, 2);
36        if ($byteOrder !== 'II' && $byteOrder !== 'MM') {
37            throw new \RuntimeException('Not a valid TIFF file');
38        }
39        $le = ($byteOrder === 'II');
40
41        $readUint16 = function (string $buf, int $offset) use ($le): int {
42            $v = unpack($le ? 'v' : 'n', substr($buf, $offset, 2))[1];
43            return $v;
44        };
45        $readUint32 = function (string $buf, int $offset) use ($le): int {
46            $v = unpack($le ? 'V' : 'N', substr($buf, $offset, 4))[1];
47            return $v;
48        };
49
50        // Check magic number
51        $magic = $readUint16($data, 2);
52        if ($magic !== 42) {
53            throw new \RuntimeException('Not a valid TIFF file (bad magic)');
54        }
55
56        // Offset to first IFD
57        $ifdOffset = $readUint32($data, 4);
58        if ($ifdOffset + 2 > $len) {
59            throw new \RuntimeException('IFD offset out of range');
60        }
61
62        $numEntries = $readUint16($data, $ifdOffset);
63        $pos = $ifdOffset + 2;
64
65        $width = 0;
66        $height = 0;
67        $bitsPerSample = 8;
68        $samplesPerPixel = 3;
69        $photometric = 2; // Default: RGB
70        $iccProfile = null;
71
72        for ($i = 0; $i < $numEntries; $i++) {
73            if ($pos + 12 > $len) {
74                break;
75            }
76
77            $tag   = $readUint16($data, $pos);
78            $type  = $readUint16($data, $pos + 2);
79            $count = $readUint32($data, $pos + 4);
80
81            // Read value (SHORT=3, LONG=4)
82            $valueOrOffset = $readUint32($data, $pos + 8);
83            if ($type === 3) {
84                $value = $readUint16($data, $pos + 8);
85            } else {
86                $value = $valueOrOffset;
87            }
88
89            switch ($tag) {
90                case 256: $width = $value;
91                    break;           // ImageWidth
92                case 257: $height = $value;
93                    break;          // ImageLength
94                case 258: $bitsPerSample = $value;
95                    break;   // BitsPerSample
96                case 277: $samplesPerPixel = $value;
97                    break; // SamplesPerPixel
98                case 262: $photometric = $value;
99                    break;     // PhotometricInterpretation
100                case 34675:                                  // InterColorProfile (ICC)
101                    if ($count > 0 && $valueOrOffset + $count <= $len) {
102                        $iccProfile = substr($data, $valueOrOffset, $count);
103                    }
104                    break;
105            }
106
107            $pos += 12;
108        }
109
110        $colorSpace = match ($photometric) {
111            0, 1 => 'DeviceGray',
112            5    => 'DeviceCMYK',
113            default => 'DeviceRGB',
114        };
115
116        return new ImageInfo(
117            width: $width,
118            height: $height,
119            colorSpace: $colorSpace,
120            bitsPerComponent: $bitsPerSample,
121            format: 'tiff',
122            hasAlpha: false,
123            iccProfile: $iccProfile,
124        );
125    }
126}