Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.79% covered (success)
96.79%
211 / 218
95.74% covered (success)
95.74%
45 / 47
CRAP
0.00% covered (danger)
0.00%
0 / 1
Text
96.79% covered (success)
96.79%
211 / 218
95.74% covered (success)
95.74%
45 / 47
106
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
3
 setString
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 setStrings
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 addStringWithOffset
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getStringsWithOffset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFillColor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setStrokeColor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setStroke
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 setRotation
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setCharWrap
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setLeading
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setAlignment
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setWrap
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setTextStream
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFont
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasFont
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFont
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 escape
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 encodeWinAnsi
50.00% covered (warning)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
2.50
 getString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStrings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFillColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStrokeColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStroke
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRotation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCharWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNumberOfWrappedLines
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLeading
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAlignment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextStream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasStrings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasCharWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasLeading
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAlignment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasTextStream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTextParams
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 startStream
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 endStream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPartialStream
92.16% covered (success)
92.16%
47 / 51
0.00% covered (danger)
0.00%
0 / 1
23.26
 getColorStream
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
9
 calculateTextMatrix
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Document\Page;
16
17use Pop\Color\Color;
18use Pop\Color\Color\ColorInterface;
19use Pop\Pdf\Build\Font\Exception as FontException;
20use Pop\Pdf\Document\Font;
21use OutOfRangeException;
22
23/**
24 * Pdf page text class
25 *
26 * @category   Pop
27 * @package    Pop\Pdf
28 * @author     Nick Sagona, III <nick@popphp.org>
29 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
30 * @license    https://www.popphp.org/license     New BSD License
31 * @version    6.0.0
32 */
33class Text
34{
35
36    /**
37     * Text string value
38     * @var ?string
39     */
40    protected ?string $string = null;
41
42    /**
43     * Raw (unescaped) text string value, used for CID glyph-ID conversion
44     * @var string
45     */
46    protected string $rawString = '';
47
48    /**
49     * Font used to render this text, resolved at compile time
50     * @var ?Font
51     */
52    protected ?Font $font = null;
53
54    /**
55     * Text strings as array for streaming
56     * @var array
57     */
58    protected array $strings = [];
59
60    /**
61     * Text strings with offset values
62     * @var array
63     */
64    protected array $stringsWithOffsets = [];
65
66    /**
67     * Text font size
68     * @var int|float
69     */
70    protected int|float $size = 12;
71
72    /**
73     * Text fill color
74     * @var ?ColorInterface
75     */
76    protected ?ColorInterface $fillColor = null;
77
78    /**
79     * Text stroke color
80     * @var ?ColorInterface
81     */
82    protected ?ColorInterface $strokeColor = null;
83
84    /**
85     * Text stroke
86     * @var array
87     */
88    protected array $stroke = [
89        'width'      => 0,
90        'dashLength' => null,
91        'dashGap'    => null
92    ];
93
94    /**
95     * Basic wrap based on character length
96     * @var int
97     */
98    protected int $charWrap = 0;
99
100    /**
101     * Leading for the lines for a character wrap
102     * @var int
103     */
104    protected int $leading = 0;
105
106    /**
107     * Text alignment object
108     * @var ?Text\Alignment
109     */
110    protected ?Text\Alignment $alignment = null;
111
112    /**
113     * Text wrap object
114     * @var ?Text\Wrap
115     */
116    protected ?Text\Wrap $wrap = null;
117
118    /**
119     * Text stream object
120     * @var ?Text\Stream
121     */
122    protected ?Text\Stream $stream = null;
123
124    /**
125     * Text parameters
126     * @var array
127     */
128    protected array $textParams = [
129        'c'    => 0,
130        'w'    => 0,
131        'h'    => 100,
132        'v'    => 100,
133        'rot'  => 0,
134        'rend' => 0
135    ];
136
137    /**
138     * Constructor
139     *
140     * Instantiate a PDF text object.
141     *
142     * @param ?string    $string
143     * @param int|float|null $size
144     * @param bool       $escape
145     */
146    public function __construct(?string $string = null, int|float|null $size = null, bool $escape = true)
147    {
148        if ($string !== null) {
149            $this->setString($string, $escape);
150        }
151        if ($size !== null) {
152            $this->setSize($size);
153        }
154    }
155
156    /**
157     * Set the text string
158     *
159     * @param  string $string
160     * @param  bool   $escape
161     * @return Text
162     */
163    public function setString(string $string, bool $escape = true): Text
164    {
165        if (function_exists('mb_strlen')) {
166            if (mb_strlen($string, 'UTF-8') < strlen($string)) {
167                $string = mb_convert_encoding($string, 'UTF-8', mb_detect_encoding($string));
168            }
169        }
170
171        $this->rawString = $string;
172        $this->string    = ($escape) ? $this->escape($string) : $string;
173
174        return $this;
175    }
176
177    /**
178     * Set the text strings
179     *
180     * @param  array $strings
181     * @param  bool  $escape
182     * @return Text
183     */
184    public function setStrings(array $strings, bool $escape = true): Text
185    {
186        if (function_exists('mb_strlen')) {
187            $strings = array_map(function($value) use ($escape) {
188                if ($value instanceof Text) {
189                    $v = $value->getString();
190                    if (mb_strlen($v, 'UTF-8') < strlen($v)) {
191                        return mb_convert_encoding($v, 'UTF-8', mb_detect_encoding($v));
192                    }
193                    $value->setString($v, $escape);
194                } else if (mb_strlen($value, 'UTF-8') < strlen($value)) {
195                    $value = mb_convert_encoding($value, 'UTF-8', mb_detect_encoding($value));
196                    if ($escape) {
197                        $value = $this->escape($value);
198                    }
199                }
200                return $value;
201            }, $strings);
202        }
203
204        $this->strings = $strings;
205        return $this;
206    }
207
208    /**
209     * Add a string with offset
210     *
211     * @param  string $string
212     * @param  int    $offset
213     * @return Text
214     */
215    public function addStringWithOffset(string $string, int $offset = 0): Text
216    {
217        if (function_exists('mb_strlen')) {
218            if (mb_strlen($string, 'UTF-8') < strlen($string)) {
219                $string = mb_convert_encoding($string, 'UTF-8', mb_detect_encoding($string));
220            }
221        }
222        $this->stringsWithOffsets[] = [
223            'string' => $string,
224            'offset' => $offset
225        ];
226        return $this;
227    }
228
229    /**
230     * Get strings with offset
231     *
232     * @return array
233     */
234    public function getStringsWithOffset(): array
235    {
236        return $this->stringsWithOffsets;
237    }
238
239    /**
240     * Set the text size
241     *
242     * @param  int|float $size
243     * @return Text
244     */
245    public function setSize(int|float $size): Text
246    {
247        $this->size = $size;
248        return $this;
249    }
250
251    /**
252     * Set the text fill color
253     *
254     * @param  ColorInterface $color
255     * @return Text
256     */
257    public function setFillColor(ColorInterface $color): Text
258    {
259        $this->fillColor = $color;
260        return $this;
261    }
262
263    /**
264     * Set the text stroke color
265     *
266     * @param  ColorInterface $color
267     * @return Text
268     */
269    public function setStrokeColor(ColorInterface $color): Text
270    {
271        $this->strokeColor = $color;
272        return $this;
273    }
274
275    /**
276     * Set the text stroke properties
277     *
278     * @param  int|float       $width
279     * @param  int|float|null $dashLength
280     * @param  int|float|null $dashGap
281     * @return Text
282     */
283    public function setStroke(int|float $width, int|float|null $dashLength = null, int|float|null $dashGap = null): Text
284    {
285        $this->stroke = [
286            'width'      => $width,
287            'dashLength' => $dashLength,
288            'dashGap'    => $dashGap
289        ];
290        return $this;
291    }
292
293    /**
294     * Method to set the rotation of the text
295     *
296     * @param  int|float $rotation
297     * @throws OutOfRangeException
298     * @return Text
299     */
300    public function setRotation(int|float $rotation): Text
301    {
302        if (abs($rotation) > 90) {
303            throw new OutOfRangeException('Error: The rotation parameter must be between -90 and 90 degrees.');
304        }
305        $this->textParams['rot'] = $rotation;
306        return $this;
307    }
308
309    /**
310     * Method to set the character wrap
311     *
312     * @param  int  $charWrap
313     * @param  ?int $leading
314     * @return Text
315     */
316    public function setCharWrap(int $charWrap, ?int $leading = null): Text
317    {
318        $this->charWrap = $charWrap;
319        if ($leading !== null) {
320            $this->setLeading($leading);
321        }
322        return $this;
323    }
324
325    /**
326     * Method to set the character wrap leading
327     *
328     * @param  int $leading
329     * @return Text
330     */
331    public function setLeading(int $leading): Text
332    {
333        $this->leading = $leading;
334        return $this;
335    }
336
337    /**
338     * Method to set the text alignment
339     *
340     * @param  Text\Alignment $alignment
341     * @return Text
342     */
343    public function setAlignment(Text\Alignment $alignment): Text
344    {
345        $this->alignment = $alignment;
346        return $this;
347    }
348
349    /**
350     * Method to set the text wrap
351     *
352     * @param  Text\Wrap $wrap
353     * @return Text
354     */
355    public function setWrap(Text\Wrap $wrap): Text
356    {
357        $this->wrap = $wrap;
358        return $this;
359    }
360
361    /**
362     * Method to set the text stream
363     *
364     * @param  Text\Stream $stream
365     * @return Text
366     */
367    public function setTextStream(Text\Stream $stream): Text
368    {
369        $this->stream = $stream;
370        return $this;
371    }
372
373    /**
374     * Set the font this text will be rendered with
375     *
376     * Resolved and set by Build\Compiler at compile time - determines
377     * whether getPartialStream() emits literal PDF strings or CID glyph-ID
378     * hex strings, and whether standard-font glyph coverage is validated.
379     *
380     * @param  Font $font
381     * @return Text
382     */
383    public function setFont(Font $font): Text
384    {
385        $this->font = $font;
386        return $this;
387    }
388
389    /**
390     * Has font
391     *
392     * @return bool
393     */
394    public function hasFont(): bool
395    {
396        return ($this->font !== null);
397    }
398
399    /**
400     * Get font
401     *
402     * @return ?Font
403     */
404    public function getFont(): ?Font
405    {
406        return $this->font;
407    }
408
409    /**
410     * Escape string
411     *
412     * @param  string $subject
413     * @param  mixed  $search
414     * @param  mixed  $replace
415     * @return string
416     */
417    public static function escape(string $subject, mixed $search  = null, mixed $replace = null): string
418    {
419        if (($search === null) && ($replace === null)) {
420            $search  = ["\\", '(', ')', "\n", "\r", "\t", "\b", "\f"];
421            $replace = ["\\\\", '\(', '\)', "\\n", "\\r", "\\t", "\\b", "\\f"];
422        }
423        return str_replace($search, $replace, $subject);
424    }
425
426    /**
427     * Transcode a raw UTF-8 string to WinAnsiEncoding (Windows-1252) bytes and escape it for a literal PDF string
428     *
429     * requireGlyphCoverage() has already confirmed the font's cmap covers
430     * every character in the string, so iconv failing here should never
431     * happen in practice - the exception is a safety net against cmap/
432     * encoding drift, not an expected runtime path.
433     *
434     * @param  string $string
435     * @throws FontException
436     * @return string
437     */
438    protected function encodeWinAnsi(string $string): string
439    {
440        $encoded = @iconv('UTF-8', 'Windows-1252', $string);
441        if ($encoded === false) {
442            throw new FontException(
443                "Error: The font '" . $this->font->getName() . "' cannot represent the given text in WinAnsiEncoding."
444            );
445        }
446
447        return $this->escape($encoded);
448    }
449
450    /**
451     * Get the text string
452     *
453     * @return ?string
454     */
455    public function getString(): ?string
456    {
457        return $this->string;
458    }
459
460    /**
461     * Get the text string array
462     *
463     * @return array
464     */
465    public function getStrings(): array
466    {
467        return $this->strings;
468    }
469
470    /**
471     * Get the text size
472     *
473     * @return int|float
474     */
475    public function getSize(): int|float
476    {
477        return $this->size;
478    }
479
480    /**
481     * Get the text fill color
482     *
483     * @return ?ColorInterface
484     */
485    public function getFillColor(): ?ColorInterface
486    {
487        return $this->fillColor;
488    }
489
490    /**
491     * Get the text stroke color
492     *
493     * @return ?ColorInterface
494     */
495    public function getStrokeColor(): ?ColorInterface
496    {
497        return $this->strokeColor;
498    }
499
500    /**
501     * Get the text stroke properties
502     *
503     * @return array
504     */
505    public function getStroke(): array
506    {
507        return $this->stroke;
508    }
509
510    /**
511     * Get the rotation of the text
512     *
513     * @return int
514     */
515    public function getRotation(): int
516    {
517        return $this->textParams['rot'];
518    }
519
520    /**
521     * Get character wrap
522     *
523     * @return int
524     */
525    public function getCharWrap(): int
526    {
527        return $this->charWrap;
528    }
529
530    /**
531     * Get number of wrapped lines from character wrap
532     *
533     * @return int
534     */
535    public function getNumberOfWrappedLines(): int
536    {
537        return count(explode("\n", wordwrap($this->string, $this->charWrap, "\n")));
538    }
539
540    /**
541     * Get character wrap leading
542     *
543     * @return int
544     */
545    public function getLeading(): int
546    {
547        return $this->leading;
548    }
549
550    /**
551     * Get text alignment
552     *
553     * @return ?Text\Alignment
554     */
555    public function getAlignment(): ?Text\Alignment
556    {
557        return $this->alignment;
558    }
559
560    /**
561     * Get text wrap
562     *
563     * @return ?Text\Wrap
564     */
565    public function getWrap(): ?Text\Wrap
566    {
567        return $this->wrap;
568    }
569
570    /**
571     * Get text stream
572     *
573     * @return ?Text\Stream
574     */
575    public function getTextStream(): ?Text\Stream
576    {
577        return $this->stream;
578    }
579
580    /**
581     * Has text string
582     *
583     * @return bool
584     */
585    public function hasString(): bool
586    {
587        return ($this->string !== null);
588    }
589
590    /**
591     * Has text string array
592     *
593     * @return bool
594     */
595    public function hasStrings(): bool
596    {
597        return !empty($this->strings);
598    }
599
600    /**
601     * Has character wrap
602     *
603     * @return bool
604     */
605    public function hasCharWrap(): bool
606    {
607        return ($this->charWrap > 0);
608    }
609
610    /**
611     * Has character wrap leading
612     *
613     * @return bool
614     */
615    public function hasLeading(): bool
616    {
617        return ($this->leading > 0);
618    }
619
620    /**
621     * Has text alignment
622     *
623     * @return bool
624     */
625    public function hasAlignment(): bool
626    {
627        return ($this->alignment !== null);
628    }
629
630    /**
631     * Has text wrap
632     *
633     * @return bool
634     */
635    public function hasWrap(): bool
636    {
637        return ($this->wrap !== null);
638    }
639
640    /**
641     * Has text stream
642     *
643     * @return bool
644     */
645    public function hasTextStream(): bool
646    {
647        return ($this->stream !== null);
648    }
649
650    /**
651     * Set the text parameters for rendering text content
652     *
653     * @param  int|float $c    (character spacing)
654     * @param  int|float $w    (word spacing)
655     * @param  int|float $h    (horz stretch)
656     * @param  int|float $v    (vert stretch)
657     * @param  int $rot  (rotation, -90 - 90)
658     * @param  int $rend (render flag, 0 - 7)
659     * @throws OutOfRangeException
660     * @return Text
661     */
662    public function setTextParams(int|float $c = 0, int|float $w = 0, int|float $h = 100, int|float $v = 100, int $rot = 0, int $rend = 0): Text
663    {
664        // Check the rotation parameter.
665        if (abs($rot) > 90) {
666            throw new OutOfRangeException('Error: The rotation parameter must be between -90 and 90 degrees.');
667        }
668
669        // Check the render parameter.
670        if (($rend > 7) || ($rend < 0)) {
671            throw new OutOfRangeException('Error: The render parameter must be an integer between 0 and 7.');
672        }
673
674        // Set the text parameters.
675        $this->textParams['c']    = $c;
676        $this->textParams['w']    = $w;
677        $this->textParams['h']    = $h;
678        $this->textParams['v']    = $v;
679        $this->textParams['rot']  = $rot;
680        $this->textParams['rend'] = $rend;
681
682        return $this;
683    }
684
685    /**
686     * Start the text stream
687     *
688     * @param  string    $fontReference
689     * @param  int|float $x
690     * @param  int|float $y
691     * @return string
692     */
693    public function startStream(string $fontReference, int|float $x, int|float $y): string
694    {
695        $stream        = '';
696        $fontReference = substr($fontReference, 0, strpos($fontReference, ' '));
697
698        $stream .= "\nBT\n    {$fontReference} {$this->size} Tf\n    " . $this->calculateTextMatrix() .
699            " {$x} {$y} Tm\n    " . $this->textParams['c'] . " Tc " . $this->textParams['w'] .
700            " Tw " . $this->textParams['rend'] . " Tr\n";
701
702        return $stream;
703    }
704
705    /**
706     * End the text stream
707     *
708     * @return string
709     */
710    public function endStream(): string
711    {
712        return "ET\n";
713    }
714
715    /**
716     * Get the text stream
717     *
718     * @param  string    $fontReference
719     * @param  int|float $x
720     * @param  int|float $y
721     * @return string
722     */
723    public function getStream(string $fontReference, int|float $x, int|float $y): string
724    {
725        return $this->startStream($fontReference, $x, $y) . $this->getPartialStream() . $this->endStream();
726    }
727
728    /**
729     * Get the partial text stream
730     *
731     * @param  ?string $fontReference
732     * @return string
733     */
734    public function getPartialStream(?string $fontReference = null): string
735    {
736        $stream = '';
737
738        if ($fontReference !== null) {
739            $fontReference = substr($fontReference, 0, strpos($fontReference, ' '));
740            $stream       .= "    {$fontReference} {$this->size} Tf\n";
741        }
742
743        $stream .= $this->getColorStream();
744
745        $isCid      = ($this->font !== null) && $this->font->isCid();
746        $isStandard = ($this->font !== null) && $this->font->isStandard();
747
748        if ($isStandard) {
749            $this->font->requireGlyphCoverage($this->rawString);
750            foreach ($this->stringsWithOffsets as $string) {
751                $this->font->requireGlyphCoverage($string['string']);
752            }
753        }
754
755        if (count($this->stringsWithOffsets) > 0) {
756            if ($isCid) {
757                $stream .= "    [<" . $this->font->stringToGidHex($this->rawString) . ">";
758                foreach ($this->stringsWithOffsets as $string) {
759                    $stream .= " " . (0 - $string['offset']) . " <" . $this->font->stringToGidHex($string['string']) . ">";
760                }
761                $stream .= "]TJ\n";
762            } else if ($isStandard) {
763                $stream .= "    [(" . $this->encodeWinAnsi($this->rawString) . ")";
764                foreach ($this->stringsWithOffsets as $string) {
765                    $stream .= " " . (0 - $string['offset']) . " (" . $this->encodeWinAnsi($string['string']) . ")";
766                }
767                $stream .= "]TJ\n";
768            } else {
769                $stream .= "    [({$this->string})";
770                foreach ($this->stringsWithOffsets as $string) {
771                    $stream .= " " . (0 - $string['offset']) . " (" . $string['string'] . ")";
772                }
773                $stream .= "]TJ\n";
774            }
775        } else {
776            if (($this->hasCharWrap()) && (strlen($this->string) > $this->charWrap)) {
777                if ($isCid) {
778                    throw new FontException(
779                        "Error: Character wrap is not yet supported with a CID/Unicode-embedded font ('" .
780                        $this->font->getName() . "')."
781                    );
782                }
783
784                if ((int)$this->leading == 0) {
785                    $this->leading = $this->size;
786                }
787
788                if ($isStandard) {
789                    $strings = explode("\n", wordwrap($this->rawString, $this->charWrap, "\n"));
790                    foreach ($strings as $i => $string) {
791                        $stream .= "    (" . $this->encodeWinAnsi($string) . ")Tj\n";
792                        if ($i < count($strings)) {
793                            $stream .= "    0 -" . $this->leading . " Td\n";
794                        }
795                    }
796                } else {
797                    $strings = explode("\n", wordwrap($this->string, $this->charWrap, "\n"));
798                    foreach ($strings as $i => $string) {
799                        $stream .= "    ({$string})Tj\n";
800                        if ($i < count($strings)) {
801                            $stream .= "    0 -" . $this->leading . " Td\n";
802                        }
803                    }
804                }
805            } else {
806                if ($isCid) {
807                    $stream .= "    <" . $this->font->stringToGidHex($this->rawString) . ">Tj\n";
808                } else if ($isStandard) {
809                    $stream .= "    (" . $this->encodeWinAnsi($this->rawString) . ")Tj\n";
810                } else {
811                    $stream .= "    ({$this->string})Tj\n";
812                }
813            }
814        }
815
816        return $stream;
817    }
818
819    /**
820     * Get the partial color stream
821     *
822     * @return string
823     */
824    public function getColorStream(): string
825    {
826        $stream = '';
827
828        if ($this->fillColor !== null) {
829            if ($this->fillColor instanceof Color\Rgb) {
830                $stream .= '    ' . $this->fillColor->render(Color\Rgb::PERCENT) . " rg\n";
831            } else if ($this->fillColor instanceof Color\Cmyk) {
832                $stream .= '    ' . $this->fillColor->render(Color\Cmyk::PERCENT) . " k\n";
833            } else if ($this->fillColor instanceof Color\Grayscale) {
834                $stream .= '    ' . $this->fillColor->render(Color\Grayscale::PERCENT) . " g\n";
835            }
836        }
837        if ($this->strokeColor !== null) {
838            if ($this->strokeColor instanceof Color\Rgb) {
839                $stream .= '    ' . $this->strokeColor->render(Color\Rgb::PERCENT) . " RG\n";
840            } else if ($this->strokeColor instanceof Color\Cmyk) {
841                $stream .= '    ' . $this->strokeColor->render(Color\Cmyk::PERCENT) . " K\n";
842            } else if ($this->strokeColor instanceof Color\Grayscale) {
843                $stream .= '    ' . $this->strokeColor->render(Color\Grayscale::PERCENT) . " G\n";
844            }
845        }
846
847        return $stream;
848    }
849
850    /**
851     * Calculate text matrix
852     *
853     * @return string
854     */
855    protected function calculateTextMatrix(): string
856    {
857        // Define some variables.
858        $a   = '';
859        $b   = '';
860        $c   = '';
861        $d   = '';
862        $neg = null;
863
864        // Determine is the rotate parameter is negative or not.
865        $neg = ($this->textParams['rot'] < 0) ? true : false;
866
867        // Calculate the text matrix parameters.
868        $rot = abs($this->textParams['rot']);
869
870        if (($rot >= 0) && ($rot <= 45)) {
871            $factor = round(($rot / 45), 2);
872            if ($neg) {
873                $a = 1;
874                $b = '-' . $factor;
875                $c = $factor;
876                $d = 1;
877            } else {
878                $a = 1;
879                $b = $factor;
880                $c = '-' . $factor;
881                $d = 1;
882            }
883        } else if (($rot > 45) && ($rot <= 90)) {
884            $factor = round(((90 - $rot) / 45), 2);
885            if ($neg) {
886                $a = $factor;
887                $b = -1;
888                $c = 1;
889                $d = $factor;
890            } else {
891                $a = $factor;
892                $b = 1;
893                $c = -1;
894                $d = $factor;
895            }
896        }
897
898        // Adjust the text matrix parameters according to the horizontal and vertical scale parameters.
899        if ($this->textParams['h'] != 100) {
900            $a = round(($a * ($this->textParams['h'] / 100)), 2);
901            $b = round(($b * ($this->textParams['h'] / 100)), 2);
902        }
903
904        if ($this->textParams['v'] != 100) {
905            $c = round(($c * ($this->textParams['v'] / 100)), 2);
906            $d = round(($d * ($this->textParams['v'] / 100)), 2);
907        }
908
909        // Return the text matrix.
910        return "{$a} {$b} {$c} {$d}";
911    }
912
913}