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