Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
34 / 34
CRAP
100.00% covered (success)
100.00%
1 / 1
AbstractField
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
34 / 34
48
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setDefaultValue
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
 getFont
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
 getSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFontColor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFontColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setReadOnly
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setRequired
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setNoExport
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setWidth
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHeight
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBorderWidth
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getBorderWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBorderColor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getBorderColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBackgroundColor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getBackgroundColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCaption
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCaption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAppearanceCharacteristics
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 getBorderStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 rgbToPdfArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFlags
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 encryptWith
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 encryptLiteral
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
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\Field;
16
17use Pop\Color\Color;
18use Pop\Pdf\Document\Page\Text as TextHelper;
19
20/**
21 * Pdf abstract form field class
22 *
23 * @category   Pop
24 * @package    Pop\Pdf
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    6.2.0
29 */
30abstract class AbstractField implements FieldInterface
31{
32
33    /**
34     * Field name
35     * @var ?string
36     */
37    protected ?string $name = null;
38
39    /**
40     * Text field width
41     * @var ?int
42     */
43    protected ?int $width = null;
44
45    /**
46     * Text field height
47     * @var ?int
48     */
49    protected ?int $height = null;
50
51    /**
52     * Field value
53     * @var ?string
54     */
55    protected ?string $value = null;
56
57    /**
58     * Field default value
59     * @var ?string
60     */
61    protected ?string $defaultValue = null;
62
63    /**
64     * Text field font
65     * @var ?string
66     */
67    protected ?string $font = null;
68
69    /**
70     * Text field font size
71     * @var int
72     */
73    protected int $size = 12;
74
75    /**
76     * Field font color
77     * @var ?Color\ColorInterface
78     */
79    protected ?Color\ColorInterface $fontColor = null;
80
81    /**
82     * Field flag bits
83     * @var array
84     */
85    protected array $flagBits = [];
86
87    /**
88     * Field border width, in points (0 = no border)
89     * @var int
90     */
91    protected int $borderWidth = 0;
92
93    /**
94     * Field border color, as an [r, g, b] array (0-255 each)
95     * @var ?array
96     */
97    protected ?array $borderColor = null;
98
99    /**
100     * Field background color, as an [r, g, b] array (0-255 each)
101     * @var ?array
102     */
103    protected ?array $backgroundColor = null;
104
105    /**
106     * Push-button caption text, rendered as the widget's /MK /CA entry.
107     * Harmless (simply never set) on Text/Choice fields - only a push
108     * button's appearance actually uses it.
109     * @var ?string
110     */
111    protected ?string $caption = null;
112
113    /**
114     * String encryptor callable, set by encryptWith() and applied lazily
115     * by encryptLiteral()
116     * @var ?callable
117     */
118    protected $stringEncryptor = null;
119
120    /**
121     * Constructor
122     *
123     * Instantiate a PDF text field object.
124     *
125     * @param  string  $name
126     * @param  ?string $font
127     * @param  int     $size
128     */
129    public function __construct(string $name, ?string $font = null, int $size = 12)
130    {
131        $this->setName($name);
132        $this->setSize($size);
133        if ($font !== null) {
134            $this->setFont($font);
135        }
136    }
137
138    /**
139     * Set the field name
140     *
141     * @param  string $name
142     * @return AbstractField
143     */
144    public function setName(string $name): AbstractField
145    {
146        $this->name = $name;
147        return $this;
148    }
149
150    /**
151     * Set the field value
152     *
153     * @param  string $value
154     * @return AbstractField
155     */
156    public function setValue(string $value): AbstractField
157    {
158        $this->value = $value;
159        return $this;
160    }
161
162    /**
163     * Set the field default value
164     *
165     * @param  string $value
166     * @return AbstractField
167     */
168    public function setDefaultValue(string $value): AbstractField
169    {
170        $this->defaultValue = $value;
171        return $this;
172    }
173
174    /**
175     * Set the font
176     *
177     * @param  string $font
178     * @return AbstractField
179     */
180    public function setFont(string $font): AbstractField
181    {
182        $this->font = $font;
183        return $this;
184    }
185
186    /**
187     * Get the font
188     *
189     * @return ?string
190     */
191    public function getFont(): ?string
192    {
193        return $this->font;
194    }
195
196    /**
197     * Set the font size
198     *
199     * @param  int $size
200     * @return AbstractField
201     */
202    public function setSize(int $size): AbstractField
203    {
204        $this->size = $size;
205        return $this;
206    }
207
208    /**
209     * Get the font size
210     *
211     * @return int
212     */
213    public function getSize(): int
214    {
215        return $this->size;
216    }
217
218    /**
219     * Set the font color
220     *
221     * @param  Color\ColorInterface $color
222     * @return AbstractField
223     */
224    public function setFontColor(Color\ColorInterface $color): AbstractField
225    {
226        $this->fontColor = $color;
227        return $this;
228    }
229
230    /**
231     * Get the field font color
232     *
233     * @return ?Color\ColorInterface
234     */
235    public function getFontColor(): ?Color\ColorInterface
236    {
237        return $this->fontColor;
238    }
239
240    /**
241     * Set read-only
242     *
243     * @return AbstractField
244     */
245    public function setReadOnly(): AbstractField
246    {
247        if (!in_array(1, $this->flagBits)) {
248            $this->flagBits[] = 1;
249        }
250        return $this;
251    }
252
253    /**
254     * Set required
255     *
256     * @return AbstractField
257     */
258    public function setRequired(): AbstractField
259    {
260        if (!in_array(2, $this->flagBits)) {
261            $this->flagBits[] = 2;
262        }
263        return $this;
264    }
265
266    /**
267     * Set no export
268     *
269     * @return AbstractField
270     */
271    public function setNoExport(): AbstractField
272    {
273        if (!in_array(3, $this->flagBits)) {
274            $this->flagBits[] = 3;
275        }
276        return $this;
277    }
278
279    /**
280     * Get the field name
281     *
282     * @return ?string
283     */
284    public function getName(): ?string
285    {
286        return $this->name;
287    }
288
289    /**
290     * Get the field value
291     *
292     * @return ?string
293     */
294    public function getValue(): ?string
295    {
296        return $this->value;
297    }
298
299    /**
300     * Get the field default value
301     *
302     * @return ?string
303     */
304    public function getDefaultValue(): ?string
305    {
306        return $this->defaultValue;
307    }
308
309    /**
310     * Set the field width
311     *
312     * @param  int $width
313     * @return AbstractField
314     */
315    public function setWidth(int $width): AbstractField
316    {
317        $this->width = $width;
318        return $this;
319    }
320
321    /**
322     * Get the field width
323     *
324     * @return ?int
325     */
326    public function getWidth(): ?int
327    {
328        return $this->width;
329    }
330
331    /**
332     * Set the field height
333     *
334     * @param  int $height
335     * @return AbstractField
336     */
337    public function setHeight(int $height): AbstractField
338    {
339        $this->height = $height;
340        return $this;
341    }
342
343    /**
344     * Get the field height
345     *
346     * @return ?int
347     */
348    public function getHeight(): ?int
349    {
350        return $this->height;
351    }
352
353    /**
354     * Set the border width
355     *
356     * @param  int $width
357     * @return static
358     */
359    public function setBorderWidth(int $width): static
360    {
361        $this->borderWidth = $width;
362        return $this;
363    }
364
365    /**
366     * Get the border width
367     *
368     * @return int
369     */
370    public function getBorderWidth(): int
371    {
372        return $this->borderWidth;
373    }
374
375    /**
376     * Set the border color
377     *
378     * @param  array $rgb
379     * @return static
380     */
381    public function setBorderColor(array $rgb): static
382    {
383        $this->borderColor = $rgb;
384        return $this;
385    }
386
387    /**
388     * Get the border color
389     *
390     * @return ?array
391     */
392    public function getBorderColor(): ?array
393    {
394        return $this->borderColor;
395    }
396
397    /**
398     * Set the background color
399     *
400     * @param  array $rgb
401     * @return static
402     */
403    public function setBackgroundColor(array $rgb): static
404    {
405        $this->backgroundColor = $rgb;
406        return $this;
407    }
408
409    /**
410     * Get the background color
411     *
412     * @return ?array
413     */
414    public function getBackgroundColor(): ?array
415    {
416        return $this->backgroundColor;
417    }
418
419    /**
420     * Set the push-button caption (rendered as the widget's /MK /CA entry)
421     *
422     * @param  string $caption
423     * @return static
424     */
425    public function setCaption(string $caption): static
426    {
427        $this->caption = $caption;
428        return $this;
429    }
430
431    /**
432     * Get the push-button caption
433     *
434     * @return ?string
435     */
436    public function getCaption(): ?string
437    {
438        return $this->caption;
439    }
440
441    /**
442     * Build this field's /MK appearance-characteristics dictionary fragment,
443     * or an empty string when no border, background color, or caption is set
444     *
445     * @return string
446     */
447    protected function getAppearanceCharacteristics(): string
448    {
449        if (($this->borderColor === null) && ($this->backgroundColor === null) && ($this->caption === null)) {
450            return '';
451        }
452
453        $mk = '    /MK <<';
454        if ($this->borderColor !== null) {
455            $mk .= ' /BC [' . $this->rgbToPdfArray($this->borderColor) . ']';
456        }
457        if ($this->backgroundColor !== null) {
458            $mk .= ' /BG [' . $this->rgbToPdfArray($this->backgroundColor) . ']';
459        }
460        if ($this->caption !== null) {
461            // /CA is a /MK sub-entry, not one of this class's independently-
462            // encrypted literal strings (/T, /V, /TU, /TM) - so it is always
463            // parenthesized/escaped via Text::escape() for safety (the same
464            // C1-class dictionary-injection concern applies to any literal
465            // string embedded in a getStream() template), but never routed
466            // through encryptLiteral()/the document's string encryptor.
467            $mk .= ' /CA (' . TextHelper::escape($this->caption) . ')';
468        }
469        $mk .= " >>\n";
470
471        return $mk;
472    }
473
474    /**
475     * Build this field's /BS border-style dictionary fragment, or an empty
476     * string when no border width is set
477     *
478     * @return string
479     */
480    protected function getBorderStyle(): string
481    {
482        return ($this->borderWidth > 0) ? "    /BS << /W {$this->borderWidth} >>\n" : '';
483    }
484
485    /**
486     * Convert an [r, g, b] (0-255) array into a PDF DeviceRGB component array (0-1)
487     *
488     * @param  array $rgb
489     * @return string
490     */
491    protected function rgbToPdfArray(array $rgb): string
492    {
493        return round($rgb[0] / 255, 3) . ' ' . round($rgb[1] / 255, 3) . ' ' . round($rgb[2] / 255, 3);
494    }
495
496    /**
497     * Get the flags value
498     *
499     * @return int
500     */
501    protected function getFlags(): int
502    {
503        $flags = '';
504
505        for ($i = 1; $i <= 32; $i++) {
506            $flags = ((in_array($i, $this->flagBits)) ? '1' : '0') . $flags;
507        }
508
509        return bindec($flags);
510    }
511
512    /**
513     * Encrypt this field's literal strings for a compiled, encrypted
514     * document. Called by Build\Compiler::prepareFields() before
515     * getStream().
516     *
517     * @param  callable $encryptor
518     * @return static
519     */
520    public function encryptWith(callable $encryptor): static
521    {
522        $this->stringEncryptor = $encryptor;
523        return $this;
524    }
525
526    /**
527     * Encrypt (if a document encryptor was set) a raw plaintext value and
528     * always PDF-literal-string-escape the result before it is embedded as
529     * "(...)" in a getStream() template.
530     *
531     * Always takes RAW, unescaped plaintext - never a value some caller has
532     * already escaped - because escaping and encrypting are genuinely
533     * ordered operations: encrypting an already-escaped string would
534     * encrypt the wrong bytes (the backslash-escapes themselves), so a
535     * reader would decrypt back to the escaped form, not the true value.
536     *
537     * Always escapes its own return value, even when no encryptor is set.
538     * This is a deliberate, small side effect beyond pure encryption
539     * support: /T, /TU, /TM, and /DA never escaped their value at all
540     * before this behavior existed (unlike /V and /DV, which already called
541     * Text::escape() directly). Centralizing escaping into this one helper
542     * fixes that pre-existing gap for every caller uniformly, since an
543     * unencrypted value containing "(" ")" or "\" was already just as
544     * capable of corrupting the surrounding dictionary syntax as an
545     * encrypted one is.
546     *
547     * @param  string $plaintext
548     * @return string
549     */
550    protected function encryptLiteral(string $plaintext): string
551    {
552        $value = ($this->stringEncryptor !== null) ? ($this->stringEncryptor)($plaintext) : $plaintext;
553        return TextHelper::escape($value);
554    }
555
556}