Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
89.02% |
146 / 164 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
Type1 | |
89.02% |
146 / 164 |
|
40.00% |
2 / 5 |
51.05 | |
0.00% |
0 / 1 |
__construct | |
50.00% |
10 / 20 |
|
0.00% |
0 / 1 |
19.12 | |||
parsePfb | |
94.05% |
79 / 84 |
|
0.00% |
0 / 1 |
19.08 | |||
parseAfm | |
93.33% |
42 / 45 |
|
0.00% |
0 / 1 |
12.04 | |||
convertToHex | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
strip | |
100.00% |
11 / 11 |
|
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 | */ |
14 | namespace Pop\Pdf\Build\Font; |
15 | |
16 | use 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 | */ |
28 | class 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 | } |