Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
FontInfo
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
6 / 6
20
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolve
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 resolveType0
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 resolveSimple
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 extractFontFile2
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 parseCMapStream
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Extract\Font;
16
17use Pop\Pdf\Extract\Document;
18use Pop\Pdf\Extract\Filter\Registry;
19use Pop\Pdf\Extract\Value;
20
21/**
22 * Pdf extract font info class
23 *
24 * @category   Pop
25 * @package    Pop\Pdf
26 * @author     Nick Sagona, III <nick@popphp.org>
27 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    6.0.0
30 */
31class FontInfo
32{
33
34    /**
35     * Constructor
36     *
37     * Instantiate a resolved font info value object.
38     *
39     * @param bool    $isType0           Whether this is a Type0/CID composite font
40     * @param mixed   $encoding          Name, Differences dict, or parsed embedded CMap array
41     * @param ?array  $toUnicodeCMap     Parsed /ToUnicode CMap, null if absent
42     * @param mixed   $cidToGidMap       'Identity' marker string, or raw CIDToGIDMap stream bytes
43     * @param ?string $embeddedFontBytes Decoded /FontFile2 bytes, null if not embedded
44     */
45    public function __construct(
46        public readonly bool $isType0,
47        public readonly mixed $encoding,
48        public readonly ?array $toUnicodeCMap,
49        public readonly mixed $cidToGidMap,
50        public readonly ?string $embeddedFontBytes
51    ) {
52    }
53
54    /**
55     * Resolve a font dict (or reference to one) into a FontInfo value object
56     *
57     * @param  Document $doc
58     * @param  mixed    $fontRef
59     * @return ?self
60     */
61    public static function resolve(Document $doc, mixed $fontRef): ?self
62    {
63        $fontDict = ($fontRef instanceof Value\Reference) ? $doc->resolve($fontRef) : $fontRef;
64
65        if (!is_array($fontDict)) {
66            return null;
67        }
68
69        $subtype = $fontDict['Subtype'] ?? null;
70        $isType0 = ($subtype instanceof Value\Name) && ($subtype->name === 'Type0');
71
72        $toUnicodeCMap = self::parseCMapStream($doc, $doc->resolve($fontDict['ToUnicode'] ?? null));
73
74        if ($isType0) {
75            return self::resolveType0($doc, $fontDict, $toUnicodeCMap);
76        }
77
78        return self::resolveSimple($doc, $fontDict, $toUnicodeCMap);
79    }
80
81    /**
82     * Resolve a Type0/CID composite font dict
83     *
84     * @param  Document $doc
85     * @param  array    $fontDict
86     * @param  ?array   $toUnicodeCMap
87     * @return self
88     */
89    protected static function resolveType0(Document $doc, array $fontDict, ?array $toUnicodeCMap): self
90    {
91        $encoding = $fontDict['Encoding'] ?? null;
92        if ($encoding instanceof Value\Reference) {
93            $encoding = $doc->resolve($encoding);
94        }
95        if ($encoding instanceof Value\Stream) {
96            $encoding = self::parseCMapStream($doc, $encoding);
97        }
98
99        $descendants = $doc->resolve($fontDict['DescendantFonts'] ?? null);
100        $cidFontDict = null;
101        if (is_array($descendants) && isset($descendants[0])) {
102            $cidFontDict = $doc->resolve($descendants[0]);
103        }
104
105        $cidToGidMap       = 'Identity';
106        $embeddedFontBytes = null;
107
108        if (is_array($cidFontDict)) {
109            $c2g = $doc->resolve($cidFontDict['CIDToGIDMap'] ?? null);
110            if ($c2g instanceof Value\Stream) {
111                $cidToGidMap = Registry::decode(
112                    $c2g->raw, $c2g->dict['Filter'] ?? null, $c2g->dict['DecodeParms'] ?? null, $doc->getDecodeBudget()
113                );
114            }
115
116            $embeddedFontBytes = self::extractFontFile2($doc, $doc->resolve($cidFontDict['FontDescriptor'] ?? null));
117        }
118
119        return new self(true, $encoding, $toUnicodeCMap, $cidToGidMap, $embeddedFontBytes);
120    }
121
122    /**
123     * Resolve a simple (non-Type0) font dict
124     *
125     * @param  Document $doc
126     * @param  array    $fontDict
127     * @param  ?array   $toUnicodeCMap
128     * @return self
129     */
130    protected static function resolveSimple(Document $doc, array $fontDict, ?array $toUnicodeCMap): self
131    {
132        $encoding = $fontDict['Encoding'] ?? null;
133        if ($encoding instanceof Value\Reference) {
134            $encoding = $doc->resolve($encoding);
135        }
136
137        $embeddedFontBytes = self::extractFontFile2($doc, $doc->resolve($fontDict['FontDescriptor'] ?? null));
138
139        return new self(false, $encoding, $toUnicodeCMap, null, $embeddedFontBytes);
140    }
141
142    /**
143     * Extract and decode a font descriptor's /FontFile2 embedded TrueType program
144     *
145     * @param  Document $doc
146     * @param  mixed    $descriptor
147     * @return ?string
148     */
149    protected static function extractFontFile2(Document $doc, mixed $descriptor): ?string
150    {
151        if (!is_array($descriptor)) {
152            return null;
153        }
154
155        $fontFile2 = $doc->resolve($descriptor['FontFile2'] ?? null);
156
157        if (!($fontFile2 instanceof Value\Stream)) {
158            return null;
159        }
160
161        return Registry::decode(
162            $fontFile2->raw, $fontFile2->dict['Filter'] ?? null, $fontFile2->dict['DecodeParms'] ?? null, $doc->getDecodeBudget()
163        );
164    }
165
166    /**
167     * Decode and parse a CMap stream (e.g. /ToUnicode or an embedded /Encoding CMap)
168     *
169     * @param  Document $doc
170     * @param  mixed    $stream
171     * @return ?array
172     */
173    protected static function parseCMapStream(Document $doc, mixed $stream): ?array
174    {
175        if (!($stream instanceof Value\Stream)) {
176            return null;
177        }
178
179        $decoded = Registry::decode($stream->raw, $stream->dict['Filter'] ?? null, $stream->dict['DecodeParms'] ?? null, $doc->getDecodeBudget());
180
181        return CMapParser::parse($decoded);
182    }
183
184}