Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.50% covered (success)
97.50%
117 / 120
91.67% covered (success)
91.67%
22 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
Parser
97.50% covered (success)
97.50%
117 / 120
91.67% covered (success)
91.67%
22 / 24
47
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 loadFromStream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFontIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFontObjectIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFontDescIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFontFileIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setCidFontObjectIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setToUnicodeIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setCompression
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFont
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFontIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFontObjectIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFontDescIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFontFileIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCidFontObjectIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getToUnicodeIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getObjects
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getFontReference
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFontName
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 isEmbeddable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCompressed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parse
98.15% covered (success)
98.15%
53 / 54
0.00% covered (danger)
0.00%
0 / 1
11
 buildCidWidths
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildToUnicodeCMap
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
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\Build\Font;
16
17use Pop\Pdf\Build\PdfObject\StreamObject;
18
19/**
20 * Pdf font parser class
21 *
22 * @category   Pop
23 * @package    Pop\Pdf
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    6.0.0
28 */
29class Parser
30{
31
32    /**
33     * Font object
34     * @var Type1|TrueType|null
35     */
36    protected Type1|TrueType|null $font = null;
37
38    /**
39     * Font reference index
40     * @var ?int
41     */
42    protected ?int $fontIndex = null;
43
44    /**
45     * Font object index
46     * @var ?int
47     */
48    protected ?int $fontObjectIndex = null;
49
50    /**
51     * Font descriptor index
52     * @var ?int
53     */
54    protected ?int $fontDescIndex = null;
55
56    /**
57     * Font file index
58     * @var ?int
59     */
60    protected ?int $fontFileIndex = null;
61
62    /**
63     * CID (descendant) font object index
64     * @var ?int
65     */
66    protected ?int $cidFontObjectIndex = null;
67
68    /**
69     * ToUnicode CMap stream object index
70     * @var ?int
71     */
72    protected ?int $toUnicodeIndex = null;
73
74    /**
75     * Font objects
76     * @var array
77     */
78    protected array $objects = [];
79
80    /**
81     * Font compression flag
82     * @var bool
83     */
84    protected bool $compression = false;
85
86    /**
87     * Constructor
88     *
89     * Instantiate a font parser object
90     *
91     * @param  string $fontFile
92     * @param  bool   $compression
93     * @throws Exception|\Pop\Utils\Exception
94     */
95    public function __construct(string $fontFile, bool $compression = false)
96    {
97        $ext = strtolower(substr($fontFile, (strrpos($fontFile, '.') + 1)));
98        switch ($ext) {
99            case 'ttf':
100                $this->font = new TrueType($fontFile);
101                break;
102            case 'otf':
103                $this->font = new TrueType\OpenType($fontFile);
104                break;
105            case 'pfb':
106                $this->font = new Type1($fontFile);
107                if ($this->font->afmPath === null) {
108                    throw new Exception('The AFM font file was not found.');
109                }
110                break;
111            default:
112                throw new Exception('That font type is not supported.');
113        }
114
115        $this->setCompression($compression);
116    }
117
118    /**
119     * Load font from stream
120     *
121     * @param  string $stream
122     * @return void
123     */
124    public static function loadFromStream(string $stream): void
125    {
126        // TO-DO
127    }
128
129    /**
130     * Set the font index
131     *
132     * @param  int $index
133     * @return Parser
134     */
135    public function setFontIndex(int $index): Parser
136    {
137        $this->fontIndex = $index;
138        return $this;
139    }
140
141    /**
142     * Set the font object index
143     *
144     * @param  int $index
145     * @return Parser
146     */
147    public function setFontObjectIndex(int $index): Parser
148    {
149        $this->fontObjectIndex = $index;
150        return $this;
151    }
152
153    /**
154     * Set the font descriptor index
155     *
156     * @param  int $index
157     * @return Parser
158     */
159    public function setFontDescIndex(int $index): Parser
160    {
161        $this->fontDescIndex = $index;
162        return $this;
163    }
164
165    /**
166     * Set the font file index
167     *
168     * @param  int $index
169     * @return Parser
170     */
171    public function setFontFileIndex(int $index): Parser
172    {
173        $this->fontFileIndex = $index;
174        return $this;
175    }
176
177    /**
178     * Set the CID (descendant) font object index
179     *
180     * @param  int $index
181     * @return Parser
182     */
183    public function setCidFontObjectIndex(int $index): Parser
184    {
185        $this->cidFontObjectIndex = $index;
186        return $this;
187    }
188
189    /**
190     * Set the ToUnicode CMap stream object index
191     *
192     * @param  int $index
193     * @return Parser
194     */
195    public function setToUnicodeIndex(int $index): Parser
196    {
197        $this->toUnicodeIndex = $index;
198        return $this;
199    }
200
201    /**
202     * Set the compression
203     *
204     * @param  bool $compression
205     * @return Parser
206     */
207    public function setCompression(bool $compression): Parser
208    {
209        $this->compression = $compression;
210        return $this;
211    }
212
213    /**
214     * Get the font object
215     *
216     * @return ?AbstractFont
217     */
218    public function getFont(): ?AbstractFont
219    {
220        return $this->font;
221    }
222
223    /**
224     * Get the font index
225     *
226     * @return ?int
227     */
228    public function getFontIndex(): ?int
229    {
230        return $this->fontIndex;
231    }
232
233    /**
234     * Get the font object index
235     *
236     * @return ?int
237     */
238    public function getFontObjectIndex(): ?int
239    {
240        return $this->fontObjectIndex;
241    }
242
243    /**
244     * Get the font descriptor index
245     *
246     * @return ?int
247     */
248    public function getFontDescIndex(): ?int
249    {
250        return $this->fontDescIndex;
251    }
252
253    /**
254     * Get the font file index
255     *
256     * @return ?int
257     */
258    public function getFontFileIndex(): ?int
259    {
260        return $this->fontFileIndex;
261    }
262
263    /**
264     * Get the CID (descendant) font object index
265     *
266     * @return ?int
267     */
268    public function getCidFontObjectIndex(): ?int
269    {
270        return $this->cidFontObjectIndex;
271    }
272
273    /**
274     * Get the ToUnicode CMap stream object index
275     *
276     * @return ?int
277     */
278    public function getToUnicodeIndex(): ?int
279    {
280        return $this->toUnicodeIndex;
281    }
282
283    /**
284     * Get the font objects
285     *
286     * @return array
287     */
288    public function getObjects(): array
289    {
290        if (count($this->objects) == 0) {
291            $this->parse();
292        }
293        return $this->objects;
294    }
295
296    /**
297     * Method to get the font reference.
298     *
299     * @return string
300     */
301    public function getFontReference(): string
302    {
303        return "/TT{$this->fontIndex} {$this->fontObjectIndex} 0 R";
304    }
305
306    /**
307     * Method to get the font name.
308     *
309     * @return string
310     */
311    public function getFontName(): string
312    {
313        if ($this->font instanceof Type1) {
314            return $this->font->info->postscriptName;
315        } elseif ($this->font instanceof TrueType) {
316            return $this->font->tables['name']->postscriptName;
317        }
318
319        throw new Exception('Error: The font type is not supported.');
320    }
321
322    /**
323     * Method to get if the font is embeddable.
324     *
325     * @return bool
326     */
327    public function isEmbeddable(): bool
328    {
329        return $this->font->embeddable;
330    }
331
332    /**
333     * Get whether or not the font objects are compressed
334     *
335     * @return bool
336     */
337    public function isCompressed(): bool
338    {
339        return $this->compression;
340    }
341
342    /**
343     * Parse the font data and create the font objects
344     *
345     * @throws Exception
346     * @return void
347     */
348    public function parse(): void
349    {
350        if (($this->fontIndex === null) || ($this->fontObjectIndex === null) ||
351            ($this->fontDescIndex === null) || ($this->fontFileIndex === null)) {
352            throw new Exception('Error: The font indices are not set');
353        }
354
355        if ($this->font instanceof Type1) {
356            $fontType     = 'Type1';
357            $fontName     = $this->font->info->postscriptName;
358            $fontFile     = 'FontFile';
359            $glyphWidths  = ['encoding' => 'StandardEncoding', 'widths' => $this->font->glyphWidths];
360            $unCompStream = $this->font->fontData;
361            $length1      = $this->font->length1;
362            $length2      = " /Length2 " . $this->font->length2 . " /Length3 0";
363
364            $this->objects[$this->fontObjectIndex] = StreamObject::parse(
365                "{$this->fontObjectIndex} 0 obj\n<<\n    /Type /Font\n    /Subtype /{$fontType}\n    /FontDescriptor " .
366                $this->fontDescIndex . " 0 R\n    /Name /TT{$this->fontIndex}\n    /BaseFont /" . $fontName .
367                "\n    /FirstChar 32\n    /LastChar 255\n    /Widths [" . implode(' ', $glyphWidths['widths']) .
368                "]\n    /Encoding /" . $glyphWidths['encoding'] . "\n>>\nendobj\n\n"
369            );
370        } elseif ($this->font instanceof TrueType) {
371            if (($this->cidFontObjectIndex === null) || ($this->toUnicodeIndex === null)) {
372                throw new Exception('Error: The CID font indices are not set');
373            }
374
375            $fontName     = $this->font->tables['name']->postscriptName;
376            $fontFile     = 'FontFile2';
377            $unCompStream = $this->font->read();
378            $length1      = strlen($unCompStream);
379            $length2      = null;
380
381            $this->objects[$this->fontObjectIndex] = StreamObject::parse(
382                "{$this->fontObjectIndex} 0 obj\n<<\n    /Type /Font\n    /Subtype /Type0\n    /Name /TT{$this->fontIndex}\n    /BaseFont /" .
383                $fontName . "\n    /Encoding /Identity-H\n    /DescendantFonts [" . $this->cidFontObjectIndex .
384                " 0 R]\n    /ToUnicode " . $this->toUnicodeIndex . " 0 R\n>>\nendobj\n\n"
385            );
386
387            $this->objects[$this->cidFontObjectIndex] = StreamObject::parse(
388                "{$this->cidFontObjectIndex} 0 obj\n<<\n    /Type /Font\n    /Subtype /CIDFontType2\n    /BaseFont /" .
389                $fontName . "\n    /CIDSystemInfo <</Registry (Adobe) /Ordering (Identity) /Supplement 0>>\n    /FontDescriptor " .
390                $this->fontDescIndex . " 0 R\n    /CIDToGIDMap /Identity\n    /DW " . $this->font->missingWidth .
391                "\n    /W [" . $this->buildCidWidths($this->font) . "]\n>>\nendobj\n\n"
392            );
393
394            $this->objects[$this->toUnicodeIndex] = StreamObject::parse($this->buildToUnicodeCMap($this->font));
395        } else {
396            throw new Exception('Error: The font type is not supported.');
397        }
398
399        $bBox = '[' . $this->font->bBox->xMin . ' ' . $this->font->bBox->yMin . ' ' .
400            $this->font->bBox->xMax . ' ' . $this->font->bBox->yMax . ']';
401
402        if (($this->compression) && function_exists('gzcompress')) {
403            $compStream  = gzcompress($unCompStream, 9);
404            $fontFileObj = "{$this->fontFileIndex} 0 obj\n<</Length " . strlen($compStream) .
405                " /Filter /FlateDecode /Length1 " . $length1 . $length2 . ">>\nstream\n" . $compStream . "\nendstream\nendobj\n\n";
406        } else {
407            $fontFileObj = "{$this->fontFileIndex} 0 obj\n<</Length " . strlen($unCompStream) . " /Length1 " .
408                $length1 . $length2 . ">>\nstream\n" . $unCompStream . "\nendstream\nendobj\n\n";
409        }
410
411        $this->objects[$this->fontDescIndex] = StreamObject::parse(
412            "{$this->fontDescIndex} 0 obj\n<<\n    /Type /FontDescriptor\n    /FontName /" . $fontName .
413            "\n    /{$fontFile} {$this->fontFileIndex} 0 R\n    /MissingWidth {$this->font->missingWidth}\n    /StemV " .
414            $this->font->stemV . "\n    /Flags " . $this->font->calcFlags() . "\n    /FontBBox {$bBox}\n    /Descent " .
415            $this->font->descent . "\n    /Ascent {$this->font->ascent}\n    /CapHeight " . $this->font->capHeight .
416            "\n    /ItalicAngle {$this->font->italicAngle}\n>>\nendobj\n\n"
417        );
418
419        $this->objects[$this->fontFileIndex] = StreamObject::parse($fontFileObj);
420    }
421
422    /**
423     * Build the /W (per-CID width) array for a CID font from GID-indexed glyph widths
424     *
425     * hmtx-derived glyph widths are already contiguous from GID 0, so this
426     * emits a single 'c [w1 w2 ... wn]' run rather than detecting ranges.
427     *
428     * @param  TrueType $font
429     * @return string
430     */
431    protected function buildCidWidths(TrueType $font): string
432    {
433        return '0 [' . implode(' ', $font->glyphWidths) . ']';
434    }
435
436    /**
437     * Build a /ToUnicode CMap stream mapping glyph IDs back to Unicode code units
438     *
439     * Inverts the font's own codepoint => GID cmap. The inversion is lossy:
440     * several codepoints can share one GID (e.g. times.ttf maps both U+0020
441     * and U+00A1 to GID 3), but a /ToUnicode CMap can only name one of them.
442     * The LOWEST codepoint wins, which keeps the ASCII/Latin-1 member of any
443     * such collision - so a space extracts as a space rather than as whatever
444     * exotic codepoint happened to be iterated last.
445     *
446     * @param  TrueType $font
447     * @return string
448     */
449    protected function buildToUnicodeCMap(TrueType $font): string
450    {
451        $gidToCodeUnit = [];
452        foreach (($font->cmap['glyphNumbers'] ?? []) as $codeUnit => $gid) {
453            if (!isset($gidToCodeUnit[$gid]) || ($codeUnit < $gidToCodeUnit[$gid])) {
454                $gidToCodeUnit[$gid] = $codeUnit;
455            }
456        }
457        ksort($gidToCodeUnit);
458
459        $body = '';
460        foreach (array_chunk($gidToCodeUnit, 100, true) as $chunk) {
461            $body .= count($chunk) . " beginbfchar\n";
462            foreach ($chunk as $gid => $codeUnit) {
463                $body .= sprintf("<%04X> <%04X>\n", $gid, $codeUnit);
464            }
465            $body .= "endbfchar\n";
466        }
467
468        $cmapProgram = "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n" .
469            "/CIDSystemInfo <</Registry (Adobe) /Ordering (UCS) /Supplement 0>> def\n" .
470            "/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n" .
471            "1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n" . $body .
472            "endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n";
473
474        return "{$this->toUnicodeIndex} 0 obj\n<</Length " . strlen($cmapProgram) . ">>\nstream\n" .
475            $cmapProgram . "\nendstream\nendobj\n\n";
476    }
477
478}