Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.25% covered (warning)
44.25%
77 / 174
72.73% covered (success)
72.73%
16 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
Stream
44.25% covered (warning)
44.25%
77 / 174
72.73% covered (success)
72.73%
16 / 22
1558.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setStartX
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setStartY
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setEdgeX
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setEdgeY
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setCurrentX
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setCurrentY
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getStartX
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStartY
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEdgeX
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEdgeY
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentX
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentY
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addText
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 setCurrentStyle
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 hasTextStreams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextStreams
61.90% covered (warning)
61.90%
13 / 21
0.00% covered (danger)
0.00%
0 / 1
4.88
 getStream
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
1640
 getOrphanStream
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 hasOrphans
77.14% covered (success)
77.14%
27 / 35
0.00% covered (danger)
0.00%
0 / 1
30.88
 getColorStream
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 hasOrphanIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Pdf\Document\Page\Text;
15
16use 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@noladev.com>
24 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
25 * @license    https://www.popphp.org/license     New BSD License
26 * @version    5.2.7
27 */
28class 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     * @param  bool           $newLine
240     * @return Stream
241     */
242    public function addText(string $string, int|float|null $y = null, bool $newLine = false): Stream
243    {
244        $this->streams[] = [
245            'string'  => $string,
246            'y'       => $y,
247            'newLine' => $newLine,
248        ];
249
250        return $this;
251    }
252
253    /**
254     * Set the current style
255     *
256     * @param  string                $font
257     * @param  int                   $size
258     * @param  ?Color\ColorInterface $color
259     * @param  ?string               $align
260     * @return Stream
261     */
262    public function setCurrentStyle(string $font, int $size, ?Color\ColorInterface $color = null, ?string $align = null): Stream
263    {
264        $key = (!empty($this->streams)) ? count($this->streams) : 0;
265        $this->styles[$key] = [
266            'font'  => $font,
267            'size'  => $size,
268            'color' => $color,
269            'align' => $align,
270        ];
271
272        return $this;
273    }
274
275    /**
276     * Has text streams
277     *
278     * @return bool
279     */
280    public function hasTextStreams(): bool
281    {
282        return !empty($this->streams);
283    }
284
285    /**
286     * Get text stream
287     *
288     * @return array
289     */
290    public function getTextStreams(): array
291    {
292        $streams      = $this->streams;
293        $currentFont  = 'Arial';
294        $currentSize  = 10;
295        $currentColor = new Color\Rgb(0, 0, 0);
296        $currentAlign = null;
297
298        if (isset($this->styles[0])) {
299            $currentFont  = $this->styles[0]['font'] ?? 'Arial';
300            $currentSize  = $this->styles[0]['size'] ?? 10;
301            $currentColor = $this->styles[0]['color'] ?? new Color\Rgb(0, 0, 0);
302            $currentAlign = $this->styles[0]['align'] ?? null;
303        }
304
305        foreach ($streams as $i => $stream) {
306            if (isset($this->styles[$i])) {
307                $currentFont  = $this->styles[$i]['font'] ?? $currentFont;
308                $currentSize  = $this->styles[$i]['size'] ?? $currentSize;
309                $currentColor = $this->styles[$i]['color'] ?? $currentColor;
310                $currentAlign = $this->styles[$i]['align'] ?? $currentAlign;
311            }
312            $streams[$i]['font']  = $currentFont;
313            $streams[$i]['size']  = $currentSize;
314            $streams[$i]['color'] = $currentColor;
315            $streams[$i]['align'] = $currentAlign;
316        }
317
318        return $streams;
319    }
320
321    /**
322     * Get stream
323     *
324     * @param  array $fonts
325     * @param  array $fontReferences
326     * @return string
327     */
328    public function getStream(array $fonts, array $fontReferences): string
329    {
330        if ($this->currentX === null) {
331            $this->currentX = $this->startX;
332        }
333        if ($this->currentY === null) {
334            $this->currentY = $this->startY;
335        }
336        $fontName       = null;
337        $fontReference  = null;
338        $fontSize       = null;
339        $curFont        = null;
340
341        foreach ($this->styles as $style) {
342            if (($fontReference === null) && !empty($style['font']) && isset($fontReferences[$style['font']])) {
343                $fontName      = $style['font'];
344                $fontReference = substr($fontReferences[$fontName], 0, strpos($fontReferences[$fontName], ' '));
345                $curFont       = $fonts[$fontName] ?? null;
346            }
347            if (($fontSize === null) && !empty($style['size'])) {
348                $fontSize = $style['size'];
349            }
350        }
351
352        $firstStringWidth = null;
353        if (count($this->streams) == 1) {
354            $fontName         = $this->styles[0]['font'];
355            $fontReference    = substr($fontReferences[$fontName], 0, strpos($fontReferences[$fontName], ' '));
356            $fontSize         = (!empty($this->styles[0]['size'])) ? $this->styles[0]['size'] : $fontSize;
357            $curFont          = $fonts[$fontName] ?? null;
358            $firstStringWidth = $curFont->getStringWidth($this->streams[0]['string'], $fontSize);
359        }
360
361        if (($firstStringWidth !== null) && isset($style['align']) && ($style['align'] == 'center') && ($firstStringWidth <= $this->edgeX - $this->startX)) {
362            $centerX = ($this->currentX + $this->edgeX - $this->startX - $firstStringWidth) / 2;
363            $stream  = "\nBT\n    {$fontReference} {$fontSize} Tf\n    1 0 0 1 {$centerX} {$this->currentY} Tm\n    0 Tc 0 Tw 0 Tr\n";
364            $stream .= "    {$fontReference} {$fontSize} Tf\n";
365            $stream .= "    (" . $this->streams[0]['string'] . ")Tj\n";
366        } else {
367            $stream  = "\nBT\n    {$fontReference} {$fontSize} Tf\n    1 0 0 1 {$this->currentX} {$this->currentY} Tm\n    0 Tc 0 Tw 0 Tr\n";
368
369            foreach ($this->streams as $i => $str) {
370                if (isset($this->styles[$i]) && !empty($this->styles[$i]['font']) && isset($fontReferences[$this->styles[$i]['font']])) {
371                    $fontName      = $this->styles[$i]['font'];
372                    $fontReference = substr($fontReferences[$fontName], 0, strpos($fontReferences[$fontName], ' '));
373                    $fontSize      = (!empty($this->styles[$i]['size'])) ? $this->styles[$i]['size'] : $fontSize;
374                    $curFont       = $fonts[$fontName] ?? null;
375                    $stream       .= "    {$fontReference} {$fontSize} Tf\n";
376                }
377                if (isset($this->styles[$i]) && !empty($this->styles[$i]['color'])) {
378                    $stream .= $this->getColorStream($this->styles[$i]['color']);
379                }
380
381                if ($str['newLine']) {
382                    $nextY             = ($str['y'] !== null) ? $str['y'] : $fontSize;
383                    $stream           .= "    0 -" . $nextY . " Td\n";
384                    $this->currentX    = $this->startX;
385                    $this->currentY   -= $nextY;
386                }
387
388                $curString = explode(' ', $str['string']);
389
390                foreach ($curString as $j => $string) {
391                    $newX = $this->currentX + $curFont->getStringWidth($string, $fontSize);
392                    if (($this->edgeX !== null) && (($this->currentX >= $this->edgeX) || ($newX >= $this->edgeX))) {
393                        $nextY             = ($str['y'] !== null) ? $str['y'] : $fontSize;
394                        $stream           .= "    0 -" . $nextY . " Td\n";
395                        $this->currentX    = $this->startX;
396                        $this->currentY   -= $nextY;
397                        if (($this->edgeY !== null) && ($this->currentY <= $this->edgeY) && ($this->currentX == $this->startX)) {
398                            break;
399                        }
400                    }
401
402                    if (!isset($curString[$j + 1])) {
403                        if (isset($this->streams[$i + 1]) &&
404                            preg_match('/[a-zA-Z0-9]/', substr($this->streams[$i + 1]['string'], 0, 1))) {
405                            $string .= ' ';
406                        }
407                    } else {
408                        $string .= ' ';
409                    }
410
411                    $stream .= "    (" . $string . ")Tj\n";
412                    if ($curFont !== null) {
413                        $this->currentX += $curFont->getStringWidth($string, $fontSize);
414                    }
415                }
416                if (($this->edgeY !== null) && ($this->currentY <= $this->edgeY) && ($this->currentX == $this->startX)) {
417                    $this->orphanIndex = (isset($j)) ? [$i, $j] : [$i, 0];
418                    break;
419                }
420            }
421        }
422
423
424
425
426        $stream .= "ET\n";
427
428        return $stream;
429    }
430
431    /**
432     * Resume stream from orphaned index
433     *
434     * @return Stream
435     */
436    public function getOrphanStream(): Stream
437    {
438        $offset        = array_search($this->orphanIndex[0], array_keys($this->streams));
439        $this->streams = array_slice($this->streams, $offset, null, true);
440
441        if ($this->orphanIndex[1] > 0) {
442            $strings = array_slice(explode(' ', $this->streams[$this->orphanIndex[0]]['string']), $this->orphanIndex[1], null, true);
443            $this->streams[$this->orphanIndex[0]]['string'] = implode(' ', $strings);
444        }
445
446        $this->orphanIndex = [];
447        return $this;
448    }
449
450    /**
451     * Prepare stream
452     *
453     * @param  array $fonts
454     * @return bool
455     */
456    public function hasOrphans(array $fonts): bool
457    {
458        $this->currentX = $this->startX;
459        $this->currentY = $this->startY;
460        $fontName       = null;
461        $fontSize       = null;
462        $curFont        = null;
463
464        foreach ($this->styles as $style) {
465            if (!empty($style['font'])) {
466                $fontName = $style['font'];
467                $curFont  = $fonts[$fontName] ?? null;
468            }
469            if (($fontSize === null) && !empty($style['size'])) {
470                $fontSize = $style['size'];
471            }
472        }
473
474        foreach ($this->streams as $i => $str) {
475            if (isset($this->styles[$i]) && !empty($this->styles[$i]['font'])) {
476                $fontName = $this->styles[$i]['font'];
477                $fontSize = (!empty($this->styles[$i]['size'])) ? $this->styles[$i]['size'] : $fontSize;
478                $curFont  = $fonts[$fontName] ?? null;
479            }
480
481            $curString = explode(' ', $str['string']);
482
483            foreach ($curString as $j => $string) {
484                if (($this->edgeX !== null) && ($this->currentX >= $this->edgeX)) {
485                    $nextY             = ($str['y'] !== null) ? $str['y'] : $fontSize;
486                    $this->currentX    = $this->startX;
487                    $this->currentY   -= $nextY;
488                    if (($this->edgeY !== null) && ($this->currentY <= $this->edgeY) && ($this->currentX == $this->startX)) {
489                        break;
490                    }
491                }
492
493                if (!isset($curString[$j + 1])) {
494                    if (isset($this->streams[$i + 1]) &&
495                        preg_match('/[a-zA-Z0-9]/', substr($this->streams[$i + 1]['string'], 0, 1))) {
496                        $string .= ' ';
497                    }
498                } else {
499                    $string .= ' ';
500                }
501
502                if ($curFont !== null) {
503                    $this->currentX += $curFont->getStringWidth($string, $fontSize);
504                }
505            }
506            if (($this->edgeY !== null) && ($this->currentY <= $this->edgeY) && ($this->currentX == $this->startX)) {
507                $this->orphanIndex = (isset($j)) ? [$i, $j] : [$i, 0];
508                break;
509            }
510        }
511
512        return (!empty($this->orphanIndex));
513    }
514
515    /**
516     * Get the partial color stream
517     *
518     * @param  Color\ColorInterface $color
519     * @return string
520     */
521    public function getColorStream(Color\ColorInterface $color): string
522    {
523        $stream = '';
524
525        if ($color instanceof Color\Rgb) {
526            $stream .= '    ' . $color->render(Color\Rgb::PERCENT) . " rg\n";
527        } else if ($color instanceof Color\Cmyk) {
528            $stream .= '    ' . $color->render(Color\Cmyk::PERCENT) . " k\n";
529        } else if ($color instanceof Color\Grayscale) {
530            $stream .= '    ' . $color->render(Color\Grayscale::PERCENT) . " g\n";
531        }
532
533        return $stream;
534    }
535
536    /**
537     * Check if the text stream has orphan streams due to the page bottom
538     *
539     * @return bool
540     */
541    public function hasOrphanIndex(): bool
542    {
543        return ($this->orphanIndex !== null);
544    }
545
546}