Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.92% covered (success)
96.92%
63 / 65
88.89% covered (success)
88.89%
16 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Font
96.92% covered (success)
96.92%
63 / 65
88.89% covered (success)
88.89%
16 / 18
35
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 standardFonts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFont
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 getFont
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isStandard
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isEmbedded
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 stringToCodeUnits
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 hasGlyph
80.00% covered (success)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getGlyphId
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 requireGlyphCoverage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 stringToGidHex
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 standardFontInstance
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getStandardFonts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParsedFont
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getStringWidth
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 parser
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\Document;
16
17use Pop\Pdf\Build\Font\AbstractFont;
18use Pop\Pdf\Build\Font\Exception as FontException;
19use Pop\Pdf\Build\Font\Parser;
20use Pop\Pdf\Build\Font\TrueType;
21use InvalidArgumentException;
22
23/**
24 * Pdf font class
25 *
26 * @category   Pop
27 * @package    Pop\Pdf
28 * @author     Nick Sagona, III <nick@popphp.org>
29 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
30 * @license    https://www.popphp.org/license     New BSD License
31 * @version    6.0.0
32 */
33class Font
34{
35
36    /**
37     * Standard font constants
38     */
39    const ARIAL                      = 'Arial';
40    const ARIAL_ITALIC               = 'Arial,Italic';
41    const ARIAL_BOLD                 = 'Arial,Bold';
42    const ARIAL_BOLD_ITALIC          = 'Arial,BoldItalic';
43    const COURIER                    = 'Courier';
44    const COURIER_OBLIQUE            = 'Courier-Oblique';
45    const COURIER_BOLD               = 'Courier-Bold';
46    const COURIER_BOLD_OBLIQUE       = 'Courier-BoldOblique';
47    const COURIER_NEW                = 'CourierNew';
48    const COURIER_NEW_ITALIC         = 'CourierNew,Italic';
49    const COURIER_NEW_BOLD           = 'CourierNew,Bold';
50    const COURIER_NEW_BOLD_ITALIC    = 'CourierNew,BoldItalic';
51    const HELVETICA                  = 'Helvetica';
52    const HELVETICA_OBLIQUE          = 'Helvetica-Oblique';
53    const HELVETICA_BOLD             = 'Helvetica-Bold';
54    const HELVETICA_BOLD_OBLIQUE     = 'Helvetica-BoldOblique';
55    const SYMBOL                     = 'Symbol';
56    const TIMES_ROMAN                = 'Times-Roman';
57    const TIMES_BOLD                 = 'Times-Bold';
58    const TIMES_ITALIC               = 'Times-Italic';
59    const TIMES_BOLD_ITALIC          = 'Times-BoldItalic';
60    const TIMES_NEW_ROMAN            = 'TimesNewRoman';
61    const TIMES_NEW_ROMAN_ITALIC     = 'TimesNewRoman,Italic';
62    const TIMES_NEW_ROMAN_BOLD       = 'TimesNewRoman,Bold';
63    const TIMES_NEW_ROMAN_BOLDITALIC = 'TimesNewRoman,BoldItalic';
64    const ZAPF_DINGBATS              = 'ZapfDingbats';
65
66    /**
67     * Standard PDF fonts
68     * @var array
69     */
70    protected array $standardFonts = [
71        'Arial', 'Arial,Italic', 'Arial,Bold', 'Arial,BoldItalic', 'Courier', 'CourierNew', 'Courier-Oblique',
72        'CourierNew,Italic', 'Courier-Bold', 'CourierNew,Bold', 'Courier-BoldOblique', 'CourierNew,BoldItalic',
73        'Helvetica', 'Helvetica-Oblique', 'Helvetica-Bold', 'Helvetica-BoldOblique', 'Symbol', 'Times-Roman',
74        'Times-Bold', 'Times-Italic', 'Times-BoldItalic', 'TimesNewRoman', 'TimesNewRoman,Italic',
75        'TimesNewRoman,Bold', 'TimesNewRoman,BoldItalic', 'ZapfDingbats'
76    ];
77
78    /**
79     * Font
80     * @var ?string
81     */
82    protected ?string $font = null;
83
84    /**
85     * Font name
86     * @var ?string
87     */
88    protected ?string $name = null;
89
90    /**
91     * Flag for a standard font
92     * @var bool
93     */
94    protected bool $isStandard = false;
95
96    /**
97     * Flag for an embedded font file
98     * @var bool
99     */
100    protected bool$isEmbedded = false;
101
102    /**
103     * Font parser
104     * @var ?Parser
105     */
106    protected ?Parser $parser = null;
107
108    /**
109     * Cached standard-font instance (shared by getStringWidth() and hasGlyph())
110     *
111     * Lazily built by standardFontInstance() and invalidated automatically
112     * whenever the selected standard font changes (it's only reused when it's
113     * already an instance of the currently-selected font's class).
114     *
115     * @var ?\Pop\Pdf\Build\Font\Standard\AbstractStandard
116     */
117    protected ?\Pop\Pdf\Build\Font\Standard\AbstractStandard $standardFontInstance = null;
118
119    /**
120     * Constructor
121     *
122     * Instantiate a PDF font.
123     *
124     * @param ?string $font
125     */
126    public function __construct(?string $font = null)
127    {
128        if ($font !== null) {
129            $this->setFont($font);
130        }
131    }
132
133    /**
134     * Get standard PDF fonts in an array
135     *
136     * @return array
137     */
138    public static function standardFonts(): array
139    {
140        return (new self())->getStandardFonts();
141    }
142
143    /**
144     * Set font
145     *
146     * @param  string $font
147     * @throws InvalidArgumentException|\Pop\Pdf\Build\Font\Exception
148     * @return Font
149     */
150    public function setFont(string $font): Font
151    {
152        $this->font = $font;
153        if (in_array($font, $this->standardFonts)) {
154            $this->isStandard = true;
155            $this->name       = $font;
156        } else if (file_exists($font)) {
157            $this->isEmbedded = true;
158            $this->parser     = new Parser($this->font);
159            $this->name       = $this->parser->getFontName();
160        } else {
161            throw new InvalidArgumentException(
162                "Error: The font '" . $font . "' is not valid. It must be a standard PDF font or a font file."
163            );
164        }
165
166        return $this;
167    }
168
169    /**
170     * Get font
171     *
172     * @return ?string
173     */
174    public function getFont(): ?string
175    {
176        return $this->font;
177    }
178
179    /**
180     * Get font name
181     *
182     * @return ?string
183     */
184    public function getName(): ?string
185    {
186        return $this->name;
187    }
188
189    /**
190     * Determine if the font is a standard font
191     *
192     * @return bool
193     */
194    public function isStandard(): bool
195    {
196        return $this->isStandard;
197    }
198
199    /**
200     * Determine if the font is an embedded font
201     *
202     * @return bool
203     */
204    public function isEmbedded(): bool
205    {
206        return $this->isEmbedded;
207    }
208
209    /**
210     * Determine if the font is an embedded CID (TrueType/OpenType) font
211     *
212     * Type1 (.pfb) embedded fonts and standard fonts are not CID fonts -
213     * they stay on the single-byte encoding path.
214     *
215     * @return bool
216     */
217    public function isCid(): bool
218    {
219        return $this->isEmbedded && ($this->parser->getFont() instanceof TrueType);
220    }
221
222    /**
223     * Split a UTF-8 string into its UTF-16BE code units
224     *
225     * Matches the BMP-only convention already used by getStringWidth() and
226     * Build\Font\AbstractFont::getStringWidth() - sufficient for Cyrillic,
227     * Greek, and the vast majority of non-Latin scripts short of rare
228     * astral-plane characters (e.g. some CJK extensions, emoji).
229     *
230     * @param  string $string
231     * @return array
232     */
233    public static function stringToCodeUnits(string $string): array
234    {
235        $codeUnits = [];
236        $utf16     = iconv('UTF-8', 'UTF-16BE//IGNORE', $string);
237
238        for ($i = 0; $i < strlen($utf16); $i += 2) {
239            $codeUnits[] = (ord($utf16[$i]) << 8) | ord($utf16[$i + 1]);
240        }
241
242        return $codeUnits;
243    }
244
245    /**
246     * Determine if this font has a glyph for the given UTF-16BE code unit
247     *
248     * Type1 embedded fonts are not validated (always returns true) - their
249     * glyph coverage isn't tracked by codepoint the way standard and CID
250     * fonts are.
251     *
252     * @param  int $codeUnit
253     * @return bool
254     */
255    public function hasGlyph(int $codeUnit): bool
256    {
257        if ($this->isCid()) {
258            return $this->getGlyphId($codeUnit) !== null;
259        } else if ($this->isStandard) {
260            return $this->standardFontInstance()->hasGlyph($codeUnit);
261        }
262
263        return true;
264    }
265
266    /**
267     * Get the glyph ID for the given UTF-16BE code unit in this CID font
268     *
269     * @param  int $codeUnit
270     * @return ?int
271     */
272    public function getGlyphId(int $codeUnit): ?int
273    {
274        if (!$this->isCid()) {
275            return null;
276        }
277
278        $font = $this->parser->getFont();
279        return $font['cmap']['glyphNumbers'][$codeUnit] ?? null;
280    }
281
282    /**
283     * Assert every character in the string is covered by this font, throwing otherwise
284     *
285     * @param  string $string
286     * @throws FontException
287     * @return void
288     */
289    public function requireGlyphCoverage(string $string): void
290    {
291        foreach (self::stringToCodeUnits($string) as $codeUnit) {
292            if (!$this->hasGlyph($codeUnit)) {
293                throw new FontException(sprintf(
294                    "Error: The font '%s' does not contain a glyph for character '%s' (U+%04X).",
295                    $this->getName(), mb_chr($codeUnit), $codeUnit
296                ));
297            }
298        }
299    }
300
301    /**
302     * Convert a string to a big-endian glyph-ID hex string for this CID font's content stream
303     *
304     * @param  string $string
305     * @throws FontException
306     * @return string
307     */
308    public function stringToGidHex(string $string): string
309    {
310        $this->requireGlyphCoverage($string);
311
312        $hex = '';
313        foreach (self::stringToCodeUnits($string) as $codeUnit) {
314            $hex .= sprintf('%04X', $this->getGlyphId($codeUnit));
315        }
316
317        return $hex;
318    }
319
320    /**
321     * Instantiate this font's standard-font class (shared by getStringWidth() and hasGlyph())
322     *
323     * @throws Exception
324     * @return \Pop\Pdf\Build\Font\Standard\AbstractStandard
325     */
326    protected function standardFontInstance(): \Pop\Pdf\Build\Font\Standard\AbstractStandard
327    {
328        $fontClass = '\Pop\Pdf\Build\Font\Standard\\' . str_replace([',', '-'], ['', ''], $this->name);
329        if (!class_exists($fontClass)) {
330            throw new Exception('Error: That standard font class was not found.');
331        }
332        if (!($this->standardFontInstance instanceof $fontClass)) {
333            $this->standardFontInstance = new $fontClass();
334        }
335        return $this->standardFontInstance;
336    }
337
338    /**
339     * Get available standard fonts
340     *
341     * @return array
342     */
343    public function getStandardFonts(): array
344    {
345        return $this->standardFonts;
346    }
347
348    /**
349     * Get the font parser
350     *
351     * @return ?AbstractFont
352     */
353    public function getParsedFont(): ?AbstractFont
354    {
355        return ($this->parser !== null) ? $this->parser->getFont() : null;
356    }
357
358    /**
359     * Attempt to get string width
360     *
361     * @param  string    $string
362     * @param  int|float $size
363     * @throws Exception
364     * @return mixed
365     */
366    public function getStringWidth(string $string, int|float $size): mixed
367    {
368        if ($this->parser !== null) {
369            return $this->parser->getFont()->getStringWidth($string, $size);
370        } else {
371            $font   = $this->standardFontInstance();
372            $widths = [];
373
374            $drawingString = iconv('UTF-8', 'UTF-16BE//IGNORE', $string);
375            $characters    = [];
376
377            for ($i = 0; $i < strlen($drawingString); $i++) {
378                $characters[] = (ord($drawingString[$i++]) << 8 ) | ord($drawingString[$i]);
379            }
380
381            foreach ($characters as $character) {
382                $widths[] = $font->getGlyphWidth($character);
383            }
384
385            return (array_sum($widths) / $font->getUnitsPerEm()) * $size;
386        }
387    }
388
389    /**
390     * Get the font parser
391     *
392     * @return ?Parser
393     */
394    public function parser(): ?Parser
395    {
396        return $this->parser;
397    }
398
399}