Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
ImageParser
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
3 / 3
34
100.00% covered (success)
100.00%
1 / 1
 parse
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
14
 parseString
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
14
 looksLikeSvg
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\ImageMetadata;
6
7use Phpdftk\Filesystem\LocalFilesystem;
8
9/**
10 * Detect image format from magic bytes and delegate to the format-specific parser.
11 *
12 * Extracts only header metadata (dimensions, color space, bit depth) —
13 * never decodes pixel data. This keeps image embedding fast since PDF
14 * can reference the compressed image bytes directly.
15 */
16final class ImageParser
17{
18    public static function parse(string $path): ImageInfo
19    {
20        // Read a larger prefix than the raster parsers need so the
21        // SVG sniffer (which has to skip an optional XML prolog plus
22        // doctype before the `<svg>` opening tag) gets enough bytes
23        // on the first hop. Raster parsers still match on the first
24        // few magic bytes.
25        $data = LocalFilesystem::readPrefix($path, 4096, "image file");
26        if (self::looksLikeSvg($data)) {
27            return SvgParser::parseFile($path);
28        }
29        return match (true) {
30            str_starts_with($data, "\xFF\xD8\xFF") => JpegParser::parseFile($path),
31            str_starts_with($data, "\x89PNG\r\n\x1A\n") => PngParser::parseFile($path),
32            str_starts_with($data, 'GIF87a') || str_starts_with($data, 'GIF89a') => GifParser::parseFile($path),
33            str_starts_with($data, 'II') || str_starts_with($data, 'MM') => TiffParser::parseFile($path),
34            str_starts_with($data, 'RIFF') && substr($data, 8, 4) === 'WEBP' => WebpParser::parseFile($path),
35            str_starts_with($data, "\x00\x00\x00\x0C\x6A\x50\x20\x20") || str_starts_with($data, "\xFF\x4F\xFF\x51") => Jpeg2000Parser::parseFile($path),
36            str_starts_with($data, "\x97\x4A\x42\x32\x0D\x0A\x1A\x0A") => Jbig2Parser::parseFile($path),
37            default => throw new \RuntimeException('Unsupported image format'),
38        };
39    }
40
41    public static function parseString(string $data): ImageInfo
42    {
43        if (self::looksLikeSvg($data)) {
44            return SvgParser::parse($data);
45        }
46        return match (true) {
47            str_starts_with($data, "\xFF\xD8\xFF") => JpegParser::parse($data),
48            str_starts_with($data, "\x89PNG\r\n\x1A\n") => PngParser::parse($data),
49            str_starts_with($data, 'GIF87a') || str_starts_with($data, 'GIF89a') => GifParser::parse($data),
50            str_starts_with($data, 'II') || str_starts_with($data, 'MM') => TiffParser::parse($data),
51            str_starts_with($data, 'RIFF') && substr($data, 8, 4) === 'WEBP' => WebpParser::parse($data),
52            str_starts_with($data, "\x00\x00\x00\x0C\x6A\x50\x20\x20") || str_starts_with($data, "\xFF\x4F\xFF\x51") => Jpeg2000Parser::parse($data),
53            str_starts_with($data, "\x97\x4A\x42\x32\x0D\x0A\x1A\x0A") => Jbig2Parser::parse($data),
54            default => throw new \RuntimeException('Unsupported image format'),
55        };
56    }
57
58    /**
59     * SVG sniffer: case-insensitive `<svg` somewhere in the prefix,
60     * with the file starting either with whitespace, `<?xml`, a
61     * `<!--` comment, a `<!DOCTYPE`, or directly with `<svg`. Rules
62     * out arbitrary XML/HTML where `<svg` happens to appear inside.
63     */
64    private static function looksLikeSvg(string $data): bool
65    {
66        $head = ltrim($data);
67        if ($head === '') {
68            return false;
69        }
70        if (stripos($head, '<svg') === false) {
71            return false;
72        }
73        return str_starts_with($head, '<?xml')
74            || str_starts_with($head, '<!--')
75            || stripos($head, '<!doctype') === 0
76            || stripos($head, '<svg') === 0;
77    }
78}