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