Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
48.67% covered (warning)
48.67%
73 / 150
72.73% covered (success)
72.73%
16 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
Stream
48.67% covered (warning)
48.67%
73 / 150
72.73% covered (success)
72.73%
16 / 22
1014.87
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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 setCurrentStyle
100.00% covered (success)
100.00%
7 / 7
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
64.71% covered (warning)
64.71%
11 / 17
0.00% covered (danger)
0.00%
0 / 1
4.70
 getStream
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
992
 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 (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\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@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 */
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     * @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}