Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
48.67% |
73 / 150 |
|
72.73% |
16 / 22 |
CRAP | |
0.00% |
0 / 1 |
Stream | |
48.67% |
73 / 150 |
|
72.73% |
16 / 22 |
1014.87 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
setStartX | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setStartY | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setEdgeX | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setEdgeY | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setCurrentX | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setCurrentY | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getStartX | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getStartY | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEdgeX | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEdgeY | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCurrentX | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCurrentY | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addText | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
setCurrentStyle | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
hasTextStreams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTextStreams | |
64.71% |
11 / 17 |
|
0.00% |
0 / 1 |
4.70 | |||
getStream | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
992 | |||
getOrphanStream | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
hasOrphans | |
77.14% |
27 / 35 |
|
0.00% |
0 / 1 |
30.88 | |||
getColorStream | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
hasOrphanIndex | |
0.00% |
0 / 1 |
|
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 | */ |
14 | namespace Pop\Pdf\Document\Page\Text; |
15 | |
16 | use Pop\Color\Color; |
17 | |
18 | /** |
19 | * Pdf page text stream 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 Stream |
29 | { |
30 | |
31 | /** |
32 | * Start X |
33 | * @var int|float|null |
34 | */ |
35 | protected int|float|null $startX = null; |
36 | |
37 | /** |
38 | * Start Y |
39 | * @var int|float|null |
40 | */ |
41 | protected int|float|null $startY = null; |
42 | |
43 | /** |
44 | * Edge X boundary |
45 | * @var ?int |
46 | */ |
47 | protected int|float|null $edgeX = null; |
48 | |
49 | /** |
50 | * Edge Y boundary |
51 | * @var int|float|null |
52 | */ |
53 | protected int|float|null $edgeY = null; |
54 | /** |
55 | * Current X |
56 | * @var int|float|null |
57 | */ |
58 | protected int|float|null $currentX = null; |
59 | |
60 | /** |
61 | * Current Y |
62 | * @var int|float|null |
63 | */ |
64 | protected int|float|null $currentY = null; |
65 | |
66 | /** |
67 | * Text streams |
68 | * @var array |
69 | */ |
70 | protected array $streams = []; |
71 | |
72 | /** |
73 | * Text styles |
74 | * @var array |
75 | */ |
76 | protected array $styles = []; |
77 | |
78 | /** |
79 | * Orphan index |
80 | * @var array |
81 | */ |
82 | protected array $orphanIndex = []; |
83 | |
84 | /** |
85 | * Constructor |
86 | * |
87 | * Instantiate a PDF text stream object. |
88 | * |
89 | * @param int $startX |
90 | * @param int $startY |
91 | * @param int $edgeX |
92 | * @param ?int $edgeY |
93 | */ |
94 | public function __construct(int $startX, int $startY, int $edgeX, ?int $edgeY = null) |
95 | { |
96 | $this->setStartX($startX); |
97 | $this->setStartY($startY); |
98 | $this->setEdgeX($edgeX); |
99 | $this->setEdgeY($edgeY); |
100 | } |
101 | |
102 | /** |
103 | * Set start X |
104 | * |
105 | * @param int|float $startX |
106 | * @return Stream |
107 | */ |
108 | public function setStartX(int|float $startX): Stream |
109 | { |
110 | $this->startX = $startX; |
111 | return $this; |
112 | } |
113 | |
114 | /** |
115 | * Set start Y |
116 | * |
117 | * @param int|float $startY |
118 | * @return Stream |
119 | */ |
120 | public function setStartY(int|float $startY): Stream |
121 | { |
122 | $this->startY = $startY; |
123 | return $this; |
124 | } |
125 | |
126 | /** |
127 | * Set edge X boundary |
128 | * |
129 | * @param int|float $edgeX |
130 | * @return Stream |
131 | */ |
132 | public function setEdgeX(int|float $edgeX): Stream |
133 | { |
134 | $this->edgeX = $edgeX; |
135 | return $this; |
136 | } |
137 | |
138 | /** |
139 | * Set edge Y boundary |
140 | * |
141 | * @param int|float $edgeY |
142 | * @return Stream |
143 | */ |
144 | public function setEdgeY(int|float $edgeY): Stream |
145 | { |
146 | $this->edgeY = $edgeY; |
147 | return $this; |
148 | } |
149 | |
150 | /** |
151 | * Set current X |
152 | * |
153 | * @param int|float $currentX |
154 | * @return Stream |
155 | */ |
156 | public function setCurrentX(int|float $currentX): Stream |
157 | { |
158 | $this->currentX = $currentX; |
159 | return $this; |
160 | } |
161 | |
162 | /** |
163 | * Set current Y |
164 | * |
165 | * @param int|float $currentY |
166 | * @return Stream |
167 | */ |
168 | public function setCurrentY(int|float $currentY): Stream |
169 | { |
170 | $this->currentY = $currentY; |
171 | return $this; |
172 | } |
173 | |
174 | /** |
175 | * Get start X |
176 | * |
177 | * @return int|float|null |
178 | */ |
179 | public function getStartX(): int|float|null |
180 | { |
181 | return $this->startX; |
182 | } |
183 | |
184 | /** |
185 | * Get start Y |
186 | * |
187 | * @return int|float|null |
188 | */ |
189 | public function getStartY(): int|float|null |
190 | { |
191 | return $this->startY; |
192 | } |
193 | |
194 | /** |
195 | * Get edge X boundary |
196 | * |
197 | * @return int|float|null |
198 | */ |
199 | public function getEdgeX(): int|float|null |
200 | { |
201 | return $this->edgeX; |
202 | } |
203 | |
204 | /** |
205 | * Get edge Y boundary |
206 | * |
207 | * @return int|float|null |
208 | */ |
209 | public function getEdgeY(): int|float|null |
210 | { |
211 | return $this->edgeY; |
212 | } |
213 | |
214 | /** |
215 | * Get current X |
216 | * |
217 | * @return int|float|null |
218 | */ |
219 | public function getCurrentX(): int|float|null |
220 | { |
221 | return $this->currentX; |
222 | } |
223 | |
224 | /** |
225 | * Get current Y |
226 | * |
227 | * @return int|float|null |
228 | */ |
229 | public function getCurrentY(): int|float|null |
230 | { |
231 | return $this->currentY; |
232 | } |
233 | |
234 | /** |
235 | * Add text to the stream |
236 | * |
237 | * @param string $string |
238 | * @param int|float|null $y |
239 | * @return Stream |
240 | */ |
241 | public function addText(string $string, int|float|null $y = null): Stream |
242 | { |
243 | $this->streams[] = [ |
244 | 'string' => $string, |
245 | 'y' => $y |
246 | ]; |
247 | |
248 | return $this; |
249 | } |
250 | |
251 | /** |
252 | * Set the current style |
253 | * |
254 | * @param string $font |
255 | * @param int $size |
256 | * @param ?Color\ColorInterface $color |
257 | * @return Stream |
258 | */ |
259 | public function setCurrentStyle(string $font, int $size, ?Color\ColorInterface $color = null): Stream |
260 | { |
261 | $key = (!empty($this->streams)) ? count($this->streams) : 0; |
262 | $this->styles[$key] = [ |
263 | 'font' => $font, |
264 | 'size' => $size, |
265 | 'color' => $color |
266 | ]; |
267 | |
268 | return $this; |
269 | } |
270 | |
271 | /** |
272 | * Has text streams |
273 | * |
274 | * @return bool |
275 | */ |
276 | public function hasTextStreams(): bool |
277 | { |
278 | return !empty($this->streams); |
279 | } |
280 | |
281 | /** |
282 | * Get text stream |
283 | * |
284 | * @return array |
285 | */ |
286 | public function getTextStreams(): array |
287 | { |
288 | $streams = $this->streams; |
289 | $currentFont = 'Arial'; |
290 | $currentSize = 10; |
291 | $currentColor = new Color\Rgb(0, 0, 0); |
292 | |
293 | if (isset($this->styles[0])) { |
294 | $currentFont = $this->styles[0]['font'] ?? 'Arial'; |
295 | $currentSize = $this->styles[0]['size'] ?? 10; |
296 | $currentColor = $this->styles[0]['color'] ?? new Color\Rgb(0, 0, 0); |
297 | } |
298 | |
299 | foreach ($streams as $i => $stream) { |
300 | if (isset($this->styles[$i])) { |
301 | $currentFont = $this->styles[$i]['font'] ?? $currentFont; |
302 | $currentSize = $this->styles[$i]['size'] ?? $currentSize; |
303 | $currentColor = $this->styles[$i]['color'] ?? $currentColor; |
304 | } |
305 | $streams[$i]['font'] = $currentFont; |
306 | $streams[$i]['size'] = $currentSize; |
307 | $streams[$i]['color'] = $currentColor; |
308 | } |
309 | |
310 | return $streams; |
311 | } |
312 | |
313 | /** |
314 | * Get stream |
315 | * |
316 | * @param array $fonts |
317 | * @param array $fontReferences |
318 | * @return string |
319 | */ |
320 | public function getStream(array $fonts, array $fontReferences): string |
321 | { |
322 | if ($this->currentX === null) { |
323 | $this->currentX = $this->startX; |
324 | } |
325 | if ($this->currentY === null) { |
326 | $this->currentY = $this->startY; |
327 | } |
328 | $fontName = null; |
329 | $fontReference = null; |
330 | $fontSize = null; |
331 | $curFont = null; |
332 | |
333 | foreach ($this->styles as $style) { |
334 | if (($fontReference === null) && !empty($style['font']) && isset($fontReferences[$style['font']])) { |
335 | $fontName = $style['font']; |
336 | $fontReference = substr($fontReferences[$fontName], 0, strpos($fontReferences[$fontName], ' ')); |
337 | $curFont = $fonts[$fontName] ?? null; |
338 | } |
339 | if (($fontSize === null) && !empty($style['size'])) { |
340 | $fontSize = $style['size']; |
341 | } |
342 | } |
343 | |
344 | $stream = "\nBT\n {$fontReference} {$fontSize} Tf\n 1 0 0 1 {$this->currentX} {$this->currentY} Tm\n 0 Tc 0 Tw 0 Tr\n"; |
345 | |
346 | foreach ($this->streams as $i => $str) { |
347 | if (isset($this->styles[$i]) && !empty($this->styles[$i]['font']) && isset($fontReferences[$this->styles[$i]['font']])) { |
348 | $fontName = $this->styles[$i]['font']; |
349 | $fontReference = substr($fontReferences[$fontName], 0, strpos($fontReferences[$fontName], ' ')); |
350 | $fontSize = (!empty($this->styles[$i]['size'])) ? $this->styles[$i]['size'] : $fontSize; |
351 | $curFont = $fonts[$fontName] ?? null; |
352 | $stream .= " {$fontReference} {$fontSize} Tf\n"; |
353 | } |
354 | if (isset($this->styles[$i]) && !empty($this->styles[$i]['color'])) { |
355 | $stream .= $this->getColorStream($this->styles[$i]['color']); |
356 | } |
357 | $curString = explode(' ', $str['string']); |
358 | |
359 | foreach ($curString as $j => $string) { |
360 | if (($this->edgeX !== null) && ($this->currentX >= $this->edgeX)) { |
361 | $nextY = ($str['y'] !== null) ? $str['y'] : $fontSize; |
362 | $stream .= " 0 -" . $nextY . " Td\n"; |
363 | $this->currentX = $this->startX; |
364 | $this->currentY -= $nextY; |
365 | if (($this->edgeY !== null) && ($this->currentY <= $this->edgeY) && ($this->currentX == $this->startX)) { |
366 | break; |
367 | } |
368 | } |
369 | |
370 | if (!isset($curString[$j + 1])) { |
371 | if (isset($this->streams[$i + 1]) && |
372 | preg_match('/[a-zA-Z0-9]/', substr($this->streams[$i + 1]['string'], 0, 1))) { |
373 | $string .= ' '; |
374 | } |
375 | } else { |
376 | $string .= ' '; |
377 | } |
378 | |
379 | $stream .= " (" . $string . ")Tj\n"; |
380 | if ($curFont !== null) { |
381 | $this->currentX += $curFont->getStringWidth($string, $fontSize); |
382 | } |
383 | } |
384 | if (($this->edgeY !== null) && ($this->currentY <= $this->edgeY) && ($this->currentX == $this->startX)) { |
385 | $this->orphanIndex = (isset($j)) ? [$i, $j] : [$i, 0]; |
386 | break; |
387 | } |
388 | } |
389 | |
390 | $stream .= "ET\n"; |
391 | |
392 | return $stream; |
393 | } |
394 | |
395 | /** |
396 | * Resume stream from orphaned index |
397 | * |
398 | * @return Stream |
399 | */ |
400 | public function getOrphanStream(): Stream |
401 | { |
402 | $offset = array_search($this->orphanIndex[0], array_keys($this->streams)); |
403 | $this->streams = array_slice($this->streams, $offset, null, true); |
404 | |
405 | if ($this->orphanIndex[1] > 0) { |
406 | $strings = array_slice(explode(' ', $this->streams[$this->orphanIndex[0]]['string']), $this->orphanIndex[1], null, true); |
407 | $this->streams[$this->orphanIndex[0]]['string'] = implode(' ', $strings); |
408 | } |
409 | |
410 | $this->orphanIndex = []; |
411 | return $this; |
412 | } |
413 | |
414 | /** |
415 | * Prepare stream |
416 | * |
417 | * @param array $fonts |
418 | * @return bool |
419 | */ |
420 | public function hasOrphans(array $fonts): bool |
421 | { |
422 | $this->currentX = $this->startX; |
423 | $this->currentY = $this->startY; |
424 | $fontName = null; |
425 | $fontSize = null; |
426 | $curFont = null; |
427 | |
428 | foreach ($this->styles as $style) { |
429 | if (!empty($style['font'])) { |
430 | $fontName = $style['font']; |
431 | $curFont = $fonts[$fontName] ?? null; |
432 | } |
433 | if (($fontSize === null) && !empty($style['size'])) { |
434 | $fontSize = $style['size']; |
435 | } |
436 | } |
437 | |
438 | foreach ($this->streams as $i => $str) { |
439 | if (isset($this->styles[$i]) && !empty($this->styles[$i]['font'])) { |
440 | $fontName = $this->styles[$i]['font']; |
441 | $fontSize = (!empty($this->styles[$i]['size'])) ? $this->styles[$i]['size'] : $fontSize; |
442 | $curFont = $fonts[$fontName] ?? null; |
443 | } |
444 | |
445 | $curString = explode(' ', $str['string']); |
446 | |
447 | foreach ($curString as $j => $string) { |
448 | if (($this->edgeX !== null) && ($this->currentX >= $this->edgeX)) { |
449 | $nextY = ($str['y'] !== null) ? $str['y'] : $fontSize; |
450 | $this->currentX = $this->startX; |
451 | $this->currentY -= $nextY; |
452 | if (($this->edgeY !== null) && ($this->currentY <= $this->edgeY) && ($this->currentX == $this->startX)) { |
453 | break; |
454 | } |
455 | } |
456 | |
457 | if (!isset($curString[$j + 1])) { |
458 | if (isset($this->streams[$i + 1]) && |
459 | preg_match('/[a-zA-Z0-9]/', substr($this->streams[$i + 1]['string'], 0, 1))) { |
460 | $string .= ' '; |
461 | } |
462 | } else { |
463 | $string .= ' '; |
464 | } |
465 | |
466 | if ($curFont !== null) { |
467 | $this->currentX += $curFont->getStringWidth($string, $fontSize); |
468 | } |
469 | } |
470 | if (($this->edgeY !== null) && ($this->currentY <= $this->edgeY) && ($this->currentX == $this->startX)) { |
471 | $this->orphanIndex = (isset($j)) ? [$i, $j] : [$i, 0]; |
472 | break; |
473 | } |
474 | } |
475 | |
476 | return (!empty($this->orphanIndex)); |
477 | } |
478 | |
479 | /** |
480 | * Get the partial color stream |
481 | * |
482 | * @param Color\ColorInterface $color |
483 | * @return string |
484 | */ |
485 | public function getColorStream(Color\ColorInterface $color): string |
486 | { |
487 | $stream = ''; |
488 | |
489 | if ($color instanceof Color\Rgb) { |
490 | $stream .= ' ' . $color->render(Color\Rgb::PERCENT) . " rg\n"; |
491 | } else if ($color instanceof Color\Cmyk) { |
492 | $stream .= ' ' . $color->render(Color\Cmyk::PERCENT) . " k\n"; |
493 | } else if ($color instanceof Color\Grayscale) { |
494 | $stream .= ' ' . $color->render(Color\Grayscale::PERCENT) . " g\n"; |
495 | } |
496 | |
497 | return $stream; |
498 | } |
499 | |
500 | /** |
501 | * Check if the text stream has orphan streams due to the page bottom |
502 | * |
503 | * @return bool |
504 | */ |
505 | public function hasOrphanIndex(): bool |
506 | { |
507 | return ($this->orphanIndex !== null); |
508 | } |
509 | |
510 | } |