Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
169 / 169
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
Type1
100.00% covered (success)
100.00%
169 / 169
100.00% covered (success)
100.00%
5 / 5
50
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
9
 parsePfb
100.00% covered (success)
100.00%
89 / 89
100.00% covered (success)
100.00%
1 / 1
21
 parseAfm
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
12
 convertToHex
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 strip
100.00% covered (success)
100.00%
11 / 11
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\Utils\ArrayObject as Data;
18
19/**
20 * Type1 font 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 *
29 * @property mixed $dict
30 * @property mixed $data
31 * @property ?string $hex
32 * @property mixed $encoding
33 * @property ?int $length1
34 * @property ?int $length2
35 * @property ?string $fontData
36 * @property ?string $pfbPath
37 * @property ?string $afmPath
38 */
39class Type1 extends AbstractFont
40{
41
42    /**
43     * Font properties
44     * @var array
45     */
46    protected array $properties = [
47        'info'             => null,
48        'bBox'             => null,
49        'ascent'           => 0,
50        'descent'          => 0,
51        'numberOfGlyphs'   => 0,
52        'glyphWidths'      => [],
53        'missingWidth'     => 0,
54        'numberOfHMetrics' => 0,
55        'italicAngle'      => 0,
56        'capHeight'        => 0,
57        'stemH'            => 0,
58        'stemV'            => 0,
59        'unitsPerEm'       => 1000,
60        'flags'            => null,
61        'embeddable'       => true,
62        'dict'             => null,
63        'data'             => null,
64        'hex'              => null,
65        'encoding'         => null,
66        'length1'          => null,
67        'length2'          => null,
68        'fontData'         => null,
69        'pfbPath'          => null,
70        'afmPath'          => null,
71    ];
72
73    /**
74     * Constructor
75     *
76     * Instantiate a Type1 font file object based on a pre-existing font file on disk.
77     *
78     * @param  ?string $fontFile
79     * @param  ?string $fontStream
80     * @throws Exception|\Pop\Utils\Exception
81     */
82    public function __construct(?string $fontFile = null, ?string $fontStream = null)
83    {
84        parent::__construct($fontFile, $fontStream);
85
86        $dir = realpath($this->dir);
87
88        if (strtolower($this->extension) == 'pfb') {
89            $this->properties['pfbPath'] = $this->fullpath;
90            $this->parsePfb($this->fullpath);
91            if (file_exists($dir . DIRECTORY_SEPARATOR . $this->filename . '.afm')) {
92                $this->properties['afmPath'] = $dir . DIRECTORY_SEPARATOR . $this->filename . '.afm';
93            } else if (file_exists($dir . DIRECTORY_SEPARATOR . $this->filename . '.AFM')) {
94                $this->properties['afmPath'] = $dir . DIRECTORY_SEPARATOR . $this->filename . '.AFM';
95            }
96            if ($this->properties['afmPath'] !== null) {
97                $this->parseAfm($this->properties['afmPath']);
98            }
99        } else if (strtolower($this->extension) == 'afm') {
100            $this->properties['afmPath'] = $this->fullpath;
101            $this->parseAfm($this->properties['afmPath']);
102            if (file_exists($dir . DIRECTORY_SEPARATOR . $this->filename . '.pfb')) {
103                $this->properties['pfbPath'] = $dir . DIRECTORY_SEPARATOR . $this->filename . '.pfb';
104            } else if (file_exists($dir . DIRECTORY_SEPARATOR . $this->filename . '.PFB')) {
105                $this->properties['pfbPath'] = $dir . DIRECTORY_SEPARATOR . $this->filename . '.PFB';
106            }
107            if ($this->properties['pfbPath'] !== null) {
108                $this->parsePfb($this->properties['pfbPath']);
109            }
110        }
111    }
112
113    /**
114     * Method to parse the Type1 PFB file.
115     *
116     * @param  string $pfb
117     * @throws Exception|\Pop\Utils\Exception
118     * @return void
119     */
120    protected function parsePfb(string $pfb): void
121    {
122        if (!file_exists($pfb)) {
123            throw new Exception('Error: The PFB file does not exist.');
124        }
125
126        $data     = file_get_contents($pfb);
127        $fileSize = strlen($data);
128
129        // Get lengths and data
130        $f = fopen($pfb, 'rb');
131        $a = unpack('Cmarker/Ctype/Vsize', fread($f,6));
132        $this->properties['length1'] = $a['size'];
133        if ($this->properties['length1'] > ($fileSize - ftell($f))) {
134            throw new Exception('Error: The PFB file is not in the correct format (segment 1 length is invalid).');
135        }
136        $this->properties['fontData'] = fread($f, $this->properties['length1']);
137        $a = unpack('Cmarker/Ctype/Vsize', fread($f,6));
138        $this->properties['length2'] = $a['size'];
139        if ($this->properties['length2'] > ($fileSize - ftell($f))) {
140            throw new Exception('Error: The PFB file is not in the correct format (segment 2 length is invalid).');
141        }
142        $this->properties['fontData'] .= fread($f, $this->properties['length2']);
143
144        $info = [];
145        $this->properties['dict'] = substr($data, (int)stripos($data, 'FontDirectory'));
146        $this->properties['dict'] = substr($this->properties['dict'], 0, (int)stripos($this->properties['dict'], 'currentdict end'));
147
148        $this->properties['data'] = substr($data, ((int)stripos($data, 'currentfile eexec') + 18));
149        $this->properties['data'] = substr(
150            $this->properties['data'], 0,
151            ((int)stripos($this->properties['data'], '0000000000000000000000000000000000000000000000000000000000000000') - 1)
152        );
153
154        $this->convertToHex();
155
156        if (stripos($this->properties['dict'], '/FullName') !== false) {
157            $name = substr($this->properties['dict'], (stripos($this->properties['dict'], '/FullName ') + 10));
158            $name = trim(substr($name, 0, stripos($name, 'readonly def')));
159            $info['fullName'] = $this->strip($name);
160        }
161
162        if (stripos($this->properties['dict'], '/FamilyName') !== false) {
163            $family = substr($this->properties['dict'], (stripos($this->properties['dict'], '/FamilyName ') + 12));
164            $family = trim(substr($family, 0, stripos($family, 'readonly def')));
165            $info['fontFamily'] = $this->strip($family);
166        }
167
168        if (stripos($this->properties['dict'], '/FontName') !== false) {
169            $font = substr($this->properties['dict'], (stripos($this->properties['dict'], '/FontName ') + 10));
170            $font = trim(substr($font, 0, stripos($font, 'def')));
171            $info['postscriptName'] = $this->strip($font);
172        }
173
174        if (stripos($this->properties['dict'], '/version') !== false) {
175            $version = substr($this->properties['dict'], (stripos($this->properties['dict'], '/version ') + 9));
176            $version = trim(substr($version, 0, stripos($version, 'readonly def')));
177            $info['version'] = $this->strip($version);
178        }
179
180        if (stripos($this->properties['dict'], '/UniqueId') !== false) {
181            $matches = [];
182            preg_match('/UniqueID\s\d/', $this->properties['dict'], $matches, PREG_OFFSET_CAPTURE);
183            $id = substr($this->properties['dict'], ($matches[0][1] + 9));
184            $id = trim(substr($id, 0, stripos($id, 'def')));
185            $info['uniqueId'] = $this->strip($id);
186        }
187
188        if (stripos($this->properties['dict'], '/Notice') !== false) {
189            $copyright = substr($this->properties['dict'], (stripos($this->properties['dict'], '/Notice ') + 8));
190            $copyright = substr($copyright, 0, stripos($copyright, 'readonly def'));
191            $copyright = str_replace('\\(', '(', $copyright);
192            $copyright = trim(str_replace('\\)', ')', $copyright));
193            $info['copyright'] = $this->strip($copyright);
194        }
195
196        $this->properties['info'] = new Data($info);
197
198        if (stripos($this->properties['dict'], '/FontBBox') !== false) {
199            $bBox = substr($this->properties['dict'], (stripos($this->properties['dict'], '/FontBBox') + 9));
200            $bBox = substr($bBox, 0, stripos($bBox, 'readonly def'));
201            $bBox = trim($this->strip($bBox));
202            $bBoxAry = explode(' ', $bBox);
203            $this->properties['bBox'] = new Data([
204                'xMin' => str_replace('{', '', $bBoxAry[0]),
205                'yMin' => $bBoxAry[1],
206                'xMax' => $bBoxAry[2],
207                'yMax' => str_replace('}', '', $bBoxAry[3])
208            ]);
209        }
210
211        if (stripos($this->properties['dict'], '/Ascent') !== false) {
212            $ascent = substr($this->properties['dict'], (stripos($this->properties['dict'], '/ascent ') + 8));
213            $this->properties['ascent'] = trim(substr($ascent, 0, stripos($ascent, 'def')));
214        }
215
216        if (stripos($this->properties['dict'], '/Descent') !== false) {
217            $descent = substr($this->properties['dict'], (stripos($this->properties['dict'], '/descent ') + 9));
218            $this->properties['descent'] = trim(substr($descent, 0, stripos($descent, 'def')));
219        }
220
221        if (stripos($this->properties['dict'], '/ItalicAngle') !== false) {
222            $italic = substr($this->properties['dict'], (stripos($this->properties['dict'], '/ItalicAngle ') + 13));
223            $this->properties['italicAngle'] = trim(substr($italic, 0, stripos($italic, 'def')));
224            if ($this->properties['italicAngle'] != 0) {
225                $this->properties['flags']->isItalic = true;
226            }
227        }
228
229        if (stripos($this->properties['dict'], '/em') !== false) {
230            $units = substr($this->properties['dict'], (stripos($this->properties['dict'], '/em ') + 4));
231            $this->properties['unitsPerEm'] = trim(substr($units, 0, stripos($units, 'def')));
232        }
233
234        if (stripos($this->properties['dict'], '/isFixedPitch') !== false) {
235            $fixed = substr($this->properties['dict'], (stripos($this->properties['dict'], '/isFixedPitch ') + 14));
236            $fixed = trim(substr($fixed, 0, stripos($fixed, 'def')));
237            $this->properties['flags']->isFixedPitch = ($fixed == 'true') ? true : false;
238        }
239
240        if (stripos($this->properties['dict'], '/ForceBold') !== false) {
241            $force = substr($this->properties['dict'], (stripos($this->properties['dict'], '/ForceBold ') + 11));
242            $force = trim(substr($force, 0, stripos($force, 'def')));
243            $this->properties['flags']->isForceBold = ($force == 'true') ? true : false;
244        }
245
246        if (stripos($this->properties['dict'], '/Encoding') !== false) {
247            $enc = substr($this->properties['dict'], (stripos($this->properties['dict'], '/Encoding ') + 10));
248            $this->properties['encoding'] = trim(substr($enc, 0, stripos($enc, 'def')));
249        }
250    }
251
252    /**
253     * Method to parse the Type1 Adobe Font Metrics file
254     *
255     * @param  string $afm
256     * @throws Exception|\Pop\Utils\Exception
257     * @return void
258     */
259    protected function parseAfm(string $afm): void
260    {
261        if (!file_exists($afm)) {
262            throw new Exception('Error: The AFM file does not exist.');
263        }
264
265        $data = file_get_contents($afm);
266
267        if (stripos($data, 'FontBBox') !== false) {
268            $bBox = substr($data, (stripos($data, 'FontBBox') + 8));
269            $bBox = substr($bBox, 0, stripos($bBox, "\n"));
270            $bBox = trim($bBox);
271            $bBoxAry = explode(' ', $bBox);
272            $this->properties['bBox'] = new Data([
273                'xMin' => $bBoxAry[0],
274                'yMin' => $bBoxAry[1],
275                'xMax' => $bBoxAry[2],
276                'yMax' => $bBoxAry[3]
277            ]);
278        }
279
280        if (stripos($data, 'ItalicAngle') !== false) {
281            $ital = substr($data, (stripos($data, 'ItalicAngle ') + 11));
282            $this->properties['italicAngle'] = trim(substr($ital, 0, stripos($ital, "\n")));
283            if ($this->properties['italicAngle'] != 0) {
284                $this->properties['flags']->isItalic = true;
285            }
286        }
287
288        if (stripos($data, 'IsFixedPitch') !== false) {
289            $fixed = substr($data, (stripos($data, 'IsFixedPitch ') + 13));
290            $fixed = strtolower(trim(substr($fixed, 0, stripos($fixed, "\n"))));
291            if ($fixed == 'true') {
292                $this->properties['flags']->isFixedPitch = true;
293            }
294        }
295
296        if (stripos($data, 'CapHeight') !== false) {
297            $cap = substr($data, (stripos($data, 'CapHeight ') + 10));
298            $this->properties['capHeight'] = trim(substr($cap, 0, stripos($cap, "\n")));
299        }
300
301        if (stripos($data, 'Ascender') !== false) {
302            $asc = substr($data, (stripos($data, 'Ascender ') + 9));
303            $this->properties['ascent'] = trim(substr($asc, 0, stripos($asc, "\n")));
304        }
305
306        if (stripos($data, 'Descender') !== false) {
307            $desc = substr($data, (stripos($data, 'Descender ') + 10));
308            $this->properties['descent'] = trim(substr($desc, 0, stripos($desc, "\n")));
309        }
310
311        if (stripos($data, 'StartCharMetrics') !== false) {
312            $num = substr($data, (stripos($data, 'StartCharMetrics ') + 17));
313            $this->properties['numberOfGlyphs'] = trim(substr($num, 0, stripos($num, "\n")));
314            $chars = substr($data, (stripos($data, 'StartCharMetrics ') + 17 + strlen($this->properties['numberOfGlyphs'])));
315            $chars = trim(substr($chars, 0, stripos($chars, 'EndCharMetrics')));
316            $glyphs = explode("\n", $chars);
317            $widths = [];
318            foreach ($glyphs as $glyph) {
319                $w = substr($glyph, (stripos($glyph, 'WX ') + 3));
320                $w = substr($w, 0, strpos($w, ' ;'));
321                $widths[] = $w;
322            }
323            $this->properties['glyphWidths'] = $widths;
324        }
325    }
326
327    /**
328     * Method to convert the data string to hex.
329     *
330     * @return void
331     */
332    protected function convertToHex(): void
333    {
334        $ary = str_split($this->properties['data']);
335        $length = count($ary);
336
337        for ($i = 0; $i < $length; $i++) {
338            $this->properties['hex'] .= bin2hex($ary[$i]);
339        }
340    }
341
342    /**
343     * Method to strip parentheses et al from a string.
344     *
345     * @param  string $str
346     * @return string
347     */
348    protected function strip(string $str): string
349    {
350        // Strip parentheses
351        if (str_starts_with($str, '(')) {
352            $str = substr($str, 1);
353        }
354        if (str_ends_with($str, ')')) {
355            $str = substr($str, 0, -1);
356        }
357        // Strip curly brackets
358        if (str_starts_with($str, '{')) {
359            $str = substr($str, 1);
360        }
361        if (str_ends_with($str, '}')) {
362            $str = substr($str, 0, -1);
363        }
364        // Strip leading slash
365        if (str_starts_with($str, '/')) {
366            $str = substr($str, 1);
367        }
368
369        return $str;
370    }
371
372}