Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.24% covered (success)
95.24%
60 / 63
80.00% covered (success)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SimpleDecoder
95.24% covered (success)
95.24%
60 / 63
80.00% covered (success)
80.00%
4 / 5
33
0.00% covered (danger)
0.00%
0 / 1
 decode
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 buildTable
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
16
 buildSymbolTable
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 detectFallbackEncoding
75.00% covered (success)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
7.77
 codepointToUtf8
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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\Build\Font\TrueType;
18use Pop\Pdf\Extract\Value;
19
20/**
21 * Pdf extract simple font decoder class
22 *
23 * @category   Pop
24 * @package    Pop\Pdf
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    6.0.0
29 */
30class SimpleDecoder
31{
32
33    /**
34     * Per-FontInfo cache of built byte-to-Unicode tables
35     * @var \WeakMap
36     */
37    protected static \WeakMap $tableCache;
38
39    /**
40     * Decode a simple (non-Type0) font's raw byte string to Unicode text
41     *
42     * @param  string   $rawBytes
43     * @param  FontInfo $info
44     * @return string
45     */
46    public static function decode(string $rawBytes, FontInfo $info): string
47    {
48        self::$tableCache ??= new \WeakMap();
49
50        if (!isset(self::$tableCache[$info])) {
51            self::$tableCache[$info] = self::buildTable($info);
52        }
53
54        $table = self::$tableCache[$info];
55        $out   = '';
56
57        for ($i = 0; $i < strlen($rawBytes); $i++) {
58            $byte = ord($rawBytes[$i]);
59
60            if (($info->toUnicodeCMap !== null) && isset($info->toUnicodeCMap['bfMappings'][$byte])) {
61                $out .= $info->toUnicodeCMap['bfMappings'][$byte];
62                continue;
63            }
64
65            $unicode = $table[$byte] ?? null;
66
67            if ($unicode !== null) {
68                $out .= self::codepointToUtf8($unicode);
69            }
70        }
71
72        return $out;
73    }
74
75    /**
76     * Build a byte-to-Unicode table for a font, applying /Differences overrides
77     *
78     * @param  FontInfo $info
79     * @return array
80     */
81    protected static function buildTable(FontInfo $info): array
82    {
83        $baseName    = null;
84        $differences = null;
85
86        if ($info->encoding instanceof Value\Name) {
87            $baseName = $info->encoding->name;
88        } elseif (is_array($info->encoding)) {
89            $base     = $info->encoding['BaseEncoding'] ?? null;
90            $baseName = ($base instanceof Value\Name) ? $base->name : null;
91
92            $diffs = $info->encoding['Differences'] ?? null;
93            if (is_array($diffs)) {
94                $differences = $diffs;
95            }
96        }
97
98        if ($baseName === null) {
99            $baseName = self::detectFallbackEncoding($info->embeddedFontBytes);
100        }
101
102        $table = match ($baseName) {
103            'MacRomanEncoding' => Encoding\MacRomanEncoding::TABLE,
104            'MacExpertEncoding', 'StandardEncoding' => Encoding\StandardEncoding::TABLE,
105            'Symbol' => self::buildSymbolTable(),
106            'ZapfDingbats' => Encoding\ZapfDingbatsEncoding::TABLE,
107            default => Encoding\WinAnsiEncoding::TABLE,
108        };
109
110        if ($differences !== null) {
111            $code = 0;
112            foreach ($differences as $entry) {
113                if (is_numeric($entry)) {
114                    $code = (int) $entry;
115                } elseif ($entry instanceof Value\Name) {
116                    $unicode = GlyphNames::resolve($entry->name);
117                    if ($unicode !== null) {
118                        $table[$code] = $unicode;
119                    }
120                    $code++;
121                }
122            }
123        }
124
125        return $table;
126    }
127
128    /**
129     * Build the Symbol font's byte-to-Unicode table
130     *
131     * @return array
132     */
133    protected static function buildSymbolTable(): array
134    {
135        $table = [];
136        for ($byte = 0x20; $byte <= 0x7A; $byte++) {
137            $unicode = Encoding\SymbolEncoding::get($byte);
138            if ($unicode !== null) {
139                $table[$byte] = $unicode;
140            }
141        }
142        return $table;
143    }
144
145    /**
146     * Guess a fallback encoding from an embedded font's own cmap table when /Encoding is absent
147     *
148     * @param  ?string $embeddedFontBytes
149     * @return string
150     */
151    protected static function detectFallbackEncoding(?string $embeddedFontBytes): string
152    {
153        if ($embeddedFontBytes === null) {
154            return 'WinAnsiEncoding';
155        }
156
157        try {
158            $font = new TrueType(null, $embeddedFontBytes);
159        } catch (\Throwable $e) {
160            return 'WinAnsiEncoding';
161        }
162
163        $cmapTable = $font->tables['cmap'] ?? null;
164        if ($cmapTable === null) {
165            return 'WinAnsiEncoding';
166        }
167
168        foreach ($cmapTable->subTables as $subTable) {
169            if (($subTable->encoding === 'Mac Roman') && ($subTable->format === 0)) {
170                return 'MacRomanEncoding';
171            }
172        }
173
174        return 'WinAnsiEncoding';
175    }
176
177    /**
178     * Convert a Unicode codepoint to a UTF-8 string
179     *
180     * @param  int $codepoint
181     * @return string
182     */
183    protected static function codepointToUtf8(int $codepoint): string
184    {
185        return @mb_convert_encoding(pack('N', $codepoint), 'UTF-8', 'UTF-32BE');
186    }
187
188}