Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
89 / 89
100.00% covered (success)
100.00%
17 / 17
CRAP
100.00% covered (success)
100.00%
1 / 1
AbstractFont
100.00% covered (success)
100.00%
89 / 89
100.00% covered (success)
100.00%
17 / 17
47
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
8
 read
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 readFixed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 readInt
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 shiftToSigned
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 toEmSpace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getWidthsForGlyphs
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getStringWidth
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 calcFlags
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 offsetSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetUnset
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 __set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __unset
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\Build\Font;
16
17use Pop\Utils\ArrayObject as Data;
18
19/**
20 * Font abstract 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 $info
30 * @property mixed $bBox
31 * @property int $ascent
32 * @property int $descent
33 * @property int $numberOfGlyphs
34 * @property array $glyphWidths
35 * @property int|float $missingWidth
36 * @property int $numberOfHMetrics
37 * @property int|float $italicAngle
38 * @property int|float $capHeight
39 * @property int $stemH
40 * @property int $stemV
41 * @property int $unitsPerEm
42 * @property mixed $flags
43 * @property bool $embeddable
44 */
45abstract class AbstractFont implements \ArrayAccess
46{
47
48    /**
49     * Font properties
50     * @var array
51     */
52    protected array $properties = [
53        'info'             => null,
54        'bBox'             => null,
55        'ascent'           => 0,
56        'descent'          => 0,
57        'numberOfGlyphs'   => 0,
58        'glyphWidths'      => [],
59        'missingWidth'     => 0,
60        'numberOfHMetrics' => 0,
61        'italicAngle'      => 0,
62        'capHeight'        => 0,
63        'stemH'            => 0,
64        'stemV'            => 0,
65        'unitsPerEm'       => 1000,
66        'flags'            => null,
67        'embeddable'       => true,
68    ];
69
70    /**
71     * Read-only properties
72     * @var array
73     */
74    protected array $readOnly = [];
75
76    /**
77     * Array of allowed file types.
78     * @var array
79     */
80    protected array $allowedTypes = [
81        'afm' => 'application/x-font-afm',
82        'otf' => 'application/x-font-otf',
83        'pfb' => 'application/x-font-pfb',
84        'pfm' => 'application/x-font-pfm',
85        'ttf' => 'application/x-font-ttf'
86    ];
87
88    /**
89     * Full path of font file, i.e. '/path/to/fontfile.ext'
90     * @var ?string
91     */
92    protected ?string $fullpath = null;
93
94    /**
95     * Full, absolute directory of the font file, i.e. '/some/dir/'
96     * @var ?string
97     */
98    protected ?string $dir = null;
99
100    /**
101     * Full basename of font file, i.e. 'fontfile.ext'
102     * @var ?string
103     */
104    protected ?string $basename = null;
105
106    /**
107     * Full filename of font file, i.e. 'fontfile'
108     * @var ?string
109     */
110    protected ?string $filename = null;
111
112    /**
113     * Font file extension, i.e. 'ext'
114     * @var ?string
115     */
116    protected ?string $extension = null;
117
118    /**
119     * Font file size in bytes
120     * @var int
121     */
122    protected int $size = 0;
123
124    /**
125     * Font file mime type
126     * @var string
127     */
128    protected string $mime = 'text/plain';
129
130    /**
131     * Font stream
132     * @var ?string
133     */
134    protected ?string $stream = null;
135
136    /**
137     * Constructor
138     *
139     * Instantiate a font file object based on a pre-existing font file on disk.
140     *
141     * @param  ?string $fontFile
142     * @param  ?string $fontStream
143     * @throws Exception|\Pop\Utils\Exception
144     */
145    public function __construct(?string $fontFile = null, ?string $fontStream = null)
146    {
147        $this->properties['flags'] = new Data([
148            'isFixedPitch'  => false,
149            'isSerif'       => false,
150            'isSymbolic'    => false,
151            'isScript'      => false,
152            'isNonSymbolic' => false,
153            'isItalic'      => false,
154            'isAllCap'      => false,
155            'isSmallCap'    => false,
156            'isForceBold'   => false
157        ]);
158
159        if ($fontFile !== null) {
160            if (!file_exists($fontFile)) {
161                throw new Exception('The font file does not exist.');
162            }
163
164            $this->fullpath  = $fontFile;
165            $parts           = pathinfo($fontFile);
166            $this->size      = filesize($fontFile);
167            $this->dir       = realpath($parts['dirname']);
168            $this->basename  = $parts['basename'];
169            $this->filename  = $parts['filename'];
170            $this->extension = (isset($parts['extension']) && ($parts['extension'] != '')) ? $parts['extension'] : null;
171
172            if ($this->extension === null) {
173                throw new Exception('Error: That font file does not have an extension.');
174            }
175
176            if (!isset($this->allowedTypes[strtolower($this->extension)])) {
177                throw new Exception('Error: That font file type is not allowed.');
178            }
179
180            $this->mime = $this->allowedTypes[strtolower($this->extension)];
181        } else if ($fontStream !== null) {
182            $this->stream = $fontStream;
183        } else {
184            throw new Exception('Error: You must pass either a font file or font stream.');
185        }
186    }
187
188    /**
189     * Read data from the font file.
190     *
191     * @param  ?int $offset
192     * @param  ?int $length
193     * @return ?string
194     */
195    public function read(?int $offset = null, ?int $length = null): ?string
196    {
197        if ($offset !== null) {
198            if ($this->stream !== null) {
199                $data = (($length !== null) && ((int)$length >= 0)) ?
200                    substr($this->stream, $offset, $length) :
201                    substr($this->stream, $offset);
202            } else {
203                $data = (($length !== null) && ((int)$length >= 0)) ?
204                    file_get_contents($this->fullpath, false, null, $offset, $length) :
205                    file_get_contents($this->fullpath, false, null, $offset);
206            }
207        } else {
208            $data = ($this->stream !== null) ? $this->stream : file_get_contents($this->fullpath);
209        }
210
211        return $data;
212    }
213
214    /**
215     * Static method to read and return a fixed-point number
216     *
217     * @param  int    $mantissaBits
218     * @param  int    $fractionBits
219     * @param  string $bytes
220     * @return float|int
221     */
222    public function readFixed(int $mantissaBits, int $fractionBits, string $bytes): float|int
223    {
224        return $this->readInt((($mantissaBits + $fractionBits) >> 3), $bytes) / (1 << $fractionBits);
225    }
226
227    /**
228     * Static method to read and return a signed integer
229     *
230     * @param  int    $size
231     * @param  string $bytes
232     * @return int
233     */
234    public function readInt(int $size, string $bytes): int
235    {
236        $number = ord($bytes[0]);
237
238        if (($number & 0x80) == 0x80) {
239            $number = (~ $number) & 0xff;
240            for ($i = 1; $i < $size; $i++) {
241                $number = ($number << 8) | ((~ ord($bytes[$i])) & 0xff);
242            }
243            $number = ~$number;
244        } else {
245            for ($i = 1; $i < $size; $i++) {
246                $number = ($number << 8) | ord($bytes[$i]);
247            }
248        }
249
250        return $number;
251    }
252
253    /**
254     * Method to shift an unpacked signed short from little endian to big endian
255     *
256     * @param  int|array $values
257     * @return int|array
258     */
259    public function shiftToSigned(int|array $values): int|array
260    {
261        if (is_array($values)) {
262            foreach ($values as $key => $value) {
263                if ($value >= pow(2, 15)) {
264                    $values[$key] -= pow(2, 16);
265                }
266            }
267        } else {
268            if ($values >= pow(2, 15)) {
269                $values -= pow(2, 16);
270            }
271        }
272
273        return $values;
274    }
275
276    /**
277     * Method to convert a value to the representative value in EM.
278     *
279     * @param  int $value
280     * @return int
281     */
282    public function toEmSpace(int $value): int
283    {
284        return ($this->properties['unitsPerEm'] == 1000) ? $value : (int)ceil(($value / $this->properties['unitsPerEm']) * 1000);
285    }
286
287    /**
288     * Get the widths for the glyphs
289     *
290     * @param  array $glyphs
291     * @return array
292     */
293    public function getWidthsForGlyphs(array $glyphs): array
294    {
295        $widths = [];
296
297        foreach ($glyphs as $glyph) {
298            if (isset($this->properties['cmap']['glyphNumbers'][$glyph]) &&
299                isset($this->properties['rawGlyphWidths'][$this->properties['cmap']['glyphNumbers'][$glyph]])) {
300                $widths[] = $this->properties['rawGlyphWidths'][$this->properties['cmap']['glyphNumbers'][$glyph]];
301            } else {
302                $widths[] = $this->properties['missingWidth'];
303            }
304        }
305
306        return $widths;
307    }
308
309    /**
310     * Attempt to get string width
311     *
312     * @param  string $string
313     * @param  mixed  $size
314     * @return mixed
315     */
316    public function getStringWidth(string $string, mixed $size): mixed
317    {
318        $width = null;
319
320        $drawingString = iconv('UTF-8', 'UTF-16BE//IGNORE', $string);
321        $characters    = [];
322
323        for ($i = 0; $i < strlen($drawingString); $i++) {
324            $characters[] = (ord($drawingString[$i++]) << 8 ) | ord($drawingString[$i]);
325        }
326
327        if (count($this->properties['rawGlyphWidths']) > 0) {
328            $widths = $this->getWidthsForGlyphs($characters);
329            $width  = (array_sum($widths) / $this->properties['unitsPerEm']) * $size;
330        }
331
332        return $width;
333    }
334
335    /**
336     * Method to calculate the font flags
337     *
338     * @return int
339     */
340    public function calcFlags(): int
341    {
342        $flags = 0;
343
344        if ($this->properties['flags']['isFixedPitch']) {
345            $flags += 1 << 0;
346        }
347
348        $flags += 1 << 5;
349
350        if ($this->properties['flags']['isItalic']) {
351            $flags += 1 << 6;
352        }
353
354        return $flags;
355    }
356
357    /**
358     * Offset set method
359     *
360     * @param  mixed $offset
361     * @param  mixed $value
362     * @return void
363     */
364    public function offsetSet(mixed $offset, mixed $value): void
365    {
366        $this->properties[$offset] = $value;
367    }
368
369    /**
370     * Offset get method
371     *
372     * @param  string $offset
373     * @return mixed
374     */
375    public function offsetGet($offset): mixed
376    {
377        return $this->properties[$offset] ?? null;
378    }
379
380    /**
381     * Offset exists method
382     *
383     * @param  mixed $offset
384     * @return bool
385     */
386    public function offsetExists($offset): bool
387    {
388        return isset($this->properties[$offset]);
389    }
390
391    /**
392     * Offset unset method
393     *
394     * @param  mixed $offset
395     * @return void
396     */
397    public function offsetUnset(mixed $offset): void
398    {
399        if (isset($this->properties[$offset])) {
400            unset($this->properties[$offset]);
401        }
402    }
403
404    /**
405     * Set method
406     *
407     * @param  string $name
408     * @param  mixed $value
409     * @return void
410     */
411    public function __set(string $name, mixed $value): void
412    {
413        $this->offsetSet($name, $value);
414    }
415
416    /**
417     * Get method
418     *
419     * @param  string $name
420     * @return mixed
421     */
422    public function __get(string $name): mixed
423    {
424        return $this->offsetGet($name);
425    }
426    /**
427     * Isset method
428     *
429     * @param  string $name
430     * @return bool
431     */
432    public function __isset(string $name): bool
433    {
434        return $this->offsetExists($name);
435    }
436    /**
437     * Unset fields[$name]
438     *
439     * @param  string $name
440     * @return void
441     */
442    public function __unset(string $name): void
443    {
444        $this->offsetUnset($name);
445    }
446
447}