Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
Button
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
12 / 12
36
100.00% covered (success)
100.00%
1 / 1
 setChecked
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isChecked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addOption
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 hasOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setNoToggleToOff
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setRadio
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setPushButton
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setRadiosInUnison
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isRadio
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isPushButton
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStream
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
19
 getParentFieldStream
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
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;
18
19/**
20 * Pdf page button field class
21 *
22 * @category   Pop
23 * @package    Pop\Pdf
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    6.2.0
28 */
29class Button extends AbstractField
30{
31
32    /**
33     * Field options
34     * @var array
35     */
36    protected array $options = [];
37
38    /**
39     * Whether this specific widget is checked - independent of getValue()/
40     * setValue(), which carry the export/on-state name, not the checked
41     * state. A radio group can have every option carrying its own distinct
42     * value while only one of them is actually checked.
43     * @var bool
44     */
45    protected bool $checked = false;
46
47    /**
48     * Set checked
49     *
50     * @param  bool $checked
51     * @return Button
52     */
53    public function setChecked(bool $checked = true): Button
54    {
55        $this->checked = $checked;
56        return $this;
57    }
58
59    /**
60     * Is checked
61     *
62     * @return bool
63     */
64    public function isChecked(): bool
65    {
66        return $this->checked;
67    }
68
69    /**
70     * Add an option
71     *
72     * @param  string $option
73     * @param  int    $xOffset
74     * @param  int    $yOffset
75     * @return Button
76     */
77    public function addOption(string $option, int $xOffset = 0, int $yOffset = 0): Button
78    {
79        $this->options[] = [
80            'option'  => $option,
81            'xOffset' => $xOffset,
82            'yOffset' => $yOffset
83        ];
84
85        return $this;
86    }
87
88    /**
89     * Has options
90     *
91     * @return bool
92     */
93    public function hasOptions(): bool
94    {
95        return (count($this->options) > 0);
96    }
97
98    /**
99     * Set no toggle to off
100     *
101     * @return Button
102     */
103    public function setNoToggleToOff(): Button
104    {
105        if (!in_array(15, $this->flagBits)) {
106            $this->flagBits[] = 15;
107        }
108        return $this;
109    }
110
111    /**
112     * Set radio
113     *
114     * @return Button
115     */
116    public function setRadio(): Button
117    {
118        if (!in_array(16, $this->flagBits)) {
119            $this->flagBits[] = 16;
120        }
121        return $this;
122    }
123
124    /**
125     * Set push button
126     *
127     * @return Button
128     */
129    public function setPushButton(): Button
130    {
131        if (!in_array(17, $this->flagBits)) {
132            $this->flagBits[] = 17;
133        }
134        return $this;
135    }
136
137    /**
138     * Set radios in unison
139     *
140     * @return Button
141     */
142    public function setRadiosInUnison(): Button
143    {
144        if (!in_array(26, $this->flagBits)) {
145            $this->flagBits[] = 26;
146        }
147        return $this;
148    }
149
150    /**
151     * Is radio
152     *
153     * @return bool
154     */
155    public function isRadio(): bool
156    {
157        return in_array(16, $this->flagBits);
158    }
159
160    /**
161     * Is push button
162     *
163     * @return bool
164     */
165    public function isPushButton(): bool
166    {
167        return in_array(17, $this->flagBits);
168    }
169
170    /**
171     * Get the field stream
172     *
173     * @param  int     $i
174     * @param  int     $pageIndex
175     * @param  ?string $fontReference
176     * @param  int     $x
177     * @param  int     $y
178     * @param  ?array  $appearance
179     * @param  ?int    $parentIndex
180     * @param  ?string $captionAppearanceRef
181     * @return string
182     */
183    public function getStream(
184        int $i, int $pageIndex, ?string $fontReference, int $x, int $y,
185        ?array $appearance = null, ?int $parentIndex = null, ?string $captionAppearanceRef = null
186    ): string
187    {
188        $text    = null;
189        $options = null;
190        $color   = '0 g';
191
192        if ($this->fontColor !== null) {
193            if ($this->fontColor instanceof Color\Rgb) {
194                $color = $this->fontColor->render(Color\Rgb::PERCENT) . " rg";
195            } else if ($this->fontColor instanceof Color\Cmyk) {
196                $color = $this->fontColor->render(Color\Cmyk::PERCENT) . " k";
197            } else if ($this->fontColor instanceof Color\Grayscale) {
198                $color = $this->fontColor->render(Color\Grayscale::PERCENT) . " g";
199            }
200        }
201
202        if ($fontReference !== null) {
203            $fontReference = substr($fontReference, 0, strpos($fontReference, ' '));
204            $text          = '    /DA(' . $this->encryptLiteral($fontReference . ' ' . $this->size . ' Tf ' . $color) . ')';
205        }
206
207        $name   = (($parentIndex === null) && ($this->name !== null)) ? '    /T(' . $this->encryptLiteral($this->name) . ')/TU(' . $this->encryptLiteral($this->name) .
208            ')/TM(' . $this->encryptLiteral($this->name) . ')' : '';
209        $flags  = (($parentIndex === null) && (count($this->flagBits) > 0)) ? "\n    /Ff " . $this->getFlags() . "\n" : null;
210        $parent = ($parentIndex !== null) ? "    /Parent {$parentIndex} 0 R\n" : '';
211        // /V and /DV are bare PDF Names here (no parens/escaping), not
212        // string literals - left untouched by encryptLiteral() on purpose.
213        $default = ($this->defaultValue !== null) ? "\n    /DV " . $this->defaultValue . "\n" : null;
214
215        if (count($this->options) > 0) {
216            $options = "    /Opt [ ";
217            foreach ($this->options as $option) {
218                $options .= '(' . $this->encryptLiteral($option['option']) . ') ';
219            }
220            $options .= "]\n";
221        }
222
223        $ap = '';
224        $as = '';
225        $stateName = null;
226        if ($appearance !== null) {
227            $stateName = ($appearance['checked']) ? $appearance['onName'] : 'Off';
228            $ap = "    /AP << /N << /" . $appearance['onName'] . " " . $appearance['onRef'] . " /Off " . $appearance['offRef'] . " >> >>\n";
229            $as = "    /AS /" . $stateName . "\n";
230        } else if ($captionAppearanceRef !== null) {
231            // A push button has one static appearance, not an on/off state
232            // dictionary - /N points directly at the caption XObject.
233            $ap = "    /AP << /N " . $captionAppearanceRef . " >>\n";
234        }
235
236        // For checkboxes/radios, /V must be the same sanitized Name /AS uses
237        // so the widget's value agrees with its appearance state. Push
238        // buttons (no $appearance) keep the original bare /V behavior.
239        $value = ($appearance !== null)
240            ? "\n    /V /" . $stateName . "\n"
241            : (($this->value !== null) ? "\n    /V " . $this->value . "\n" : null);
242
243        // Return the stream
244        return "{$i} 0 obj\n<<\n    /Type /Annot\n    /Subtype /Widget\n    /FT /Btn\n    /Rect [{$x} {$y} " .
245            ($this->width + $x) . " " . ($this->height + $y) . "]{$value}{$default}\n    /P {$pageIndex} 0 R\n{$parent}" .
246            "    \n{$text}\n{$name}\n{$flags}\n{$options}{$ap}{$as}" .
247            $this->getAppearanceCharacteristics() . $this->getBorderStyle() . ">>\nendobj\n\n";
248    }
249
250    /**
251     * Get the shared parent field stream for a radio group - has no /Rect
252     * since it is not itself a widget annotation, only the field the group's
253     * widgets point back to via their own /Parent
254     *
255     * @param  int     $i
256     * @param  ?string $checkedExportName
257     * @return string
258     */
259    public function getParentFieldStream(int $i, ?string $checkedExportName = null): string
260    {
261        $name  = ($this->name !== null) ? '    /T(' . $this->encryptLiteral($this->name) . ')' : '';
262        $flags = "    /Ff " . $this->getFlags() . "\n";
263        $value = ($checkedExportName !== null) ? "    /V /" . $checkedExportName . "\n" : '';
264
265        return "{$i} 0 obj\n<<\n    /FT /Btn\n{$name}\n{$flags}{$value}>>\nendobj\n\n";
266    }
267
268}