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