Coverage Report

Created: 2026-04-12 07:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/qpdf/libqpdf/QPDFFormFieldObjectHelper.cc
Line
Count
Source
1
#include <qpdf/QPDFFormFieldObjectHelper.hh>
2
3
#include <qpdf/AcroForm.hh>
4
5
#include <qpdf/Pipeline_private.hh>
6
#include <qpdf/Pl_QPDFTokenizer.hh>
7
#include <qpdf/QIntC.hh>
8
#include <qpdf/QPDFAcroFormDocumentHelper.hh>
9
#include <qpdf/QPDFAnnotationObjectHelper.hh>
10
#include <qpdf/QPDFObjectHandle_private.hh>
11
#include <qpdf/QPDF_private.hh>
12
#include <qpdf/QTC.hh>
13
#include <qpdf/QUtil.hh>
14
#include <cstdlib>
15
16
#include <memory>
17
18
using namespace qpdf;
19
20
using FormNode = qpdf::impl::FormNode;
21
22
const QPDFObjectHandle FormNode::null_oh;
23
24
class QPDFFormFieldObjectHelper::Members: public FormNode
25
{
26
  public:
27
    Members(QPDFObjectHandle const& oh) :
28
87.5k
        FormNode(oh)
29
87.5k
    {
30
87.5k
    }
31
};
32
33
QPDFFormFieldObjectHelper::QPDFFormFieldObjectHelper(QPDFObjectHandle o) :
34
45.9k
    QPDFObjectHelper(o),
35
45.9k
    m(std::make_shared<Members>(oh()))
36
45.9k
{
37
45.9k
}
38
39
QPDFFormFieldObjectHelper::QPDFFormFieldObjectHelper() :
40
41.6k
    QPDFObjectHelper(Null::temp()),
41
41.6k
    m(std::make_shared<Members>(QPDFObjectHandle()))
42
41.6k
{
43
41.6k
}
44
45
bool
46
QPDFFormFieldObjectHelper::isNull()
47
0
{
48
0
    return m->null();
49
0
}
50
51
QPDFFormFieldObjectHelper
52
QPDFFormFieldObjectHelper::getParent()
53
0
{
54
0
    return {Null::if_null(m->Parent().oh())};
55
0
}
56
57
QPDFFormFieldObjectHelper
58
QPDFFormFieldObjectHelper::getTopLevelField(bool* is_different)
59
0
{
60
0
    return Null::if_null(m->root_field(is_different).oh());
61
0
}
62
63
FormNode
64
FormNode::root_field(bool* is_different)
65
0
{
66
0
    if (is_different) {
67
0
        *is_different = false;
68
0
    }
69
0
    if (!obj) {
70
0
        return {};
71
0
    }
72
0
    auto rf = *this;
73
0
    size_t depth = 0; // Don't bother with loop detection until depth becomes suspicious
74
0
    QPDFObjGen::set seen;
75
0
    while (rf.Parent() && (++depth < 10 || seen.add(rf))) {
76
0
        rf = rf.Parent();
77
0
        if (is_different) {
78
0
            *is_different = true;
79
0
        }
80
0
    }
81
0
    return rf;
82
0
}
83
84
QPDFObjectHandle
85
QPDFFormFieldObjectHelper::getInheritableFieldValue(std::string const& name)
86
0
{
87
0
    return Null::if_null(m->inheritable_value<QPDFObjectHandle>(name));
88
0
}
89
90
QPDFObjectHandle const&
91
FormNode::inherited(std::string const& name, bool acroform) const
92
34.8k
{
93
34.8k
    if (!obj) {
94
1.62k
        return null_oh;
95
1.62k
    }
96
33.1k
    auto node = *this;
97
33.1k
    QPDFObjGen::set seen;
98
33.1k
    size_t depth = 0; // Don't bother with loop detection until depth becomes suspicious
99
50.0k
    while (node.Parent() && (++depth < 10 || seen.add(node))) {
100
21.8k
        node = node.Parent();
101
21.8k
        if (auto const& result = node[name]) {
102
4.97k
            return {result};
103
4.97k
        }
104
21.8k
    }
105
28.1k
    return acroform ? from_AcroForm(name) : null_oh;
106
33.1k
}
107
108
std::string
109
QPDFFormFieldObjectHelper::getInheritableFieldValueAsString(std::string const& name)
110
0
{
111
0
    return m->inheritable_string(name);
112
0
}
113
114
std::string
115
FormNode::inheritable_string(std::string const& name) const
116
15.5k
{
117
15.5k
    if (auto fv = inheritable_value<String>(name)) {
118
1.74k
        return fv.utf8_value();
119
1.74k
    }
120
13.7k
    return {};
121
15.5k
}
122
123
std::string
124
QPDFFormFieldObjectHelper::getInheritableFieldValueAsName(std::string const& name)
125
0
{
126
0
    if (auto fv = m->inheritable_value<Name>(name)) {
127
0
        return fv;
128
0
    }
129
0
    return {};
130
0
}
131
132
std::string
133
QPDFFormFieldObjectHelper::getFieldType()
134
18.8k
{
135
18.8k
    if (auto ft = m->FT()) {
136
17.5k
        return ft;
137
17.5k
    }
138
1.31k
    return {};
139
18.8k
}
140
141
std::string
142
QPDFFormFieldObjectHelper::getFullyQualifiedName()
143
0
{
144
0
    return m->fully_qualified_name();
145
0
}
146
147
std::string
148
FormNode::fully_qualified_name() const
149
19.4k
{
150
19.4k
    std::string result;
151
19.4k
    auto node = *this;
152
19.4k
    QPDFObjGen::set seen;
153
19.4k
    size_t depth = 0; // Don't bother with loop detection until depth becomes suspicious
154
95.0k
    while (node && (++depth < 10 || seen.add(node))) {
155
75.5k
        if (auto T = node.T()) {
156
69.4k
            if (!result.empty()) {
157
49.9k
                result.insert(0, 1, '.');
158
49.9k
            }
159
69.4k
            result.insert(0, T.utf8_value());
160
69.4k
        }
161
75.5k
        node = node.Parent();
162
75.5k
    }
163
19.4k
    return result;
164
19.4k
}
165
166
std::string
167
QPDFFormFieldObjectHelper::getPartialName()
168
0
{
169
0
    return m->partial_name();
170
0
}
171
172
std::string
173
FormNode::partial_name() const
174
0
{
175
0
    if (auto pn = T()) {
176
0
        return pn.utf8_value();
177
0
    }
178
0
    return {};
179
0
}
180
181
std::string
182
QPDFFormFieldObjectHelper::getAlternativeName()
183
0
{
184
0
    return m->alternative_name();
185
0
}
186
187
std::string
188
FormNode::alternative_name() const
189
0
{
190
0
    if (auto an = TU()) {
191
0
        return an.utf8_value();
192
0
    }
193
0
    return fully_qualified_name();
194
0
}
195
196
std::string
197
QPDFFormFieldObjectHelper::getMappingName()
198
0
{
199
0
    return m->mapping_name();
200
0
}
201
202
std::string
203
FormNode::mapping_name() const
204
0
{
205
0
    if (auto mn = TM()) {
206
0
        return mn.utf8_value();
207
0
    }
208
0
    return alternative_name();
209
0
}
210
211
QPDFObjectHandle
212
QPDFFormFieldObjectHelper::getValue()
213
1.43k
{
214
1.43k
    return Null::if_null(m->V<QPDFObjectHandle>());
215
1.43k
}
216
217
std::string
218
QPDFFormFieldObjectHelper::getValueAsString()
219
0
{
220
0
    return m->value();
221
0
}
222
223
std::string
224
FormNode::value() const
225
15.5k
{
226
15.5k
    return inheritable_string("/V");
227
15.5k
}
228
229
QPDFObjectHandle
230
QPDFFormFieldObjectHelper::getDefaultValue()
231
0
{
232
0
    return Null::if_null(m->DV());
233
0
}
234
235
std::string
236
QPDFFormFieldObjectHelper::getDefaultValueAsString()
237
0
{
238
0
    return m->default_value();
239
0
}
240
241
std::string
242
FormNode::default_value() const
243
0
{
244
0
    return inheritable_string("/DV");
245
0
}
246
247
QPDFObjectHandle
248
QPDFFormFieldObjectHelper::getDefaultResources()
249
15.8k
{
250
15.8k
    return Null::if_null(m->getDefaultResources());
251
15.8k
}
252
253
QPDFObjectHandle
254
FormNode::getDefaultResources()
255
25.9k
{
256
25.9k
    return from_AcroForm("/DR");
257
25.9k
}
258
259
std::string
260
QPDFFormFieldObjectHelper::getDefaultAppearance()
261
0
{
262
0
    return m->default_appearance();
263
0
}
264
265
std::string
266
FormNode::default_appearance() const
267
15.5k
{
268
15.5k
    if (auto DA = inheritable_value<String>("/DA")) {
269
12.2k
        return DA.utf8_value();
270
12.2k
    }
271
3.27k
    if (String DA = from_AcroForm("/DA")) {
272
362
        return DA.utf8_value();
273
362
    }
274
2.91k
    return {};
275
3.27k
}
276
277
int
278
QPDFFormFieldObjectHelper::getQuadding()
279
0
{
280
0
    return m->getQuadding();
281
0
}
282
283
int
284
FormNode::getQuadding()
285
0
{
286
0
    auto fv = inheritable_value<QPDFObjectHandle>("/Q");
287
0
    bool looked_in_acroform = false;
288
0
    if (!fv.isInteger()) {
289
0
        fv = from_AcroForm("/Q");
290
0
        looked_in_acroform = true;
291
0
    }
292
0
    if (fv.isInteger()) {
293
0
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper Q present", looked_in_acroform ? 0 : 1);
294
0
        return QIntC::to_int(fv.getIntValue());
295
0
    }
296
0
    return 0;
297
0
}
298
299
int
300
QPDFFormFieldObjectHelper::getFlags()
301
0
{
302
0
    return m->getFlags();
303
0
}
304
305
int
306
FormNode::getFlags()
307
8.27k
{
308
8.27k
    auto f = inheritable_value<QPDFObjectHandle>("/Ff");
309
8.27k
    return f.isInteger() ? f.getIntValueAsInt() : 0;
310
8.27k
}
311
312
bool
313
QPDFFormFieldObjectHelper::isText()
314
0
{
315
0
    return m->isText();
316
0
}
317
318
bool
319
FormNode::isText()
320
0
{
321
0
    return FT() == "/Tx";
322
0
}
323
324
bool
325
QPDFFormFieldObjectHelper::isCheckbox()
326
858
{
327
858
    return m->isCheckbox();
328
858
}
329
330
bool
331
FormNode::isCheckbox()
332
2.28k
{
333
2.28k
    return FT() == "/Btn" && (getFlags() & (ff_btn_radio | ff_btn_pushbutton)) == 0;
334
2.28k
}
335
336
bool
337
QPDFFormFieldObjectHelper::isChecked()
338
0
{
339
0
    return m->isChecked();
340
0
}
341
342
bool
343
FormNode::isChecked()
344
0
{
345
0
    return isCheckbox() && V<Name>() != "/Off";
346
0
}
347
348
bool
349
QPDFFormFieldObjectHelper::isRadioButton()
350
1.47k
{
351
1.47k
    return m->isRadioButton();
352
1.47k
}
353
354
bool
355
FormNode::isRadioButton()
356
2.62k
{
357
2.62k
    return FT() == "/Btn" && (getFlags() & ff_btn_radio) == ff_btn_radio;
358
2.62k
}
359
360
bool
361
QPDFFormFieldObjectHelper::isPushbutton()
362
0
{
363
0
    return m->isPushbutton();
364
0
}
365
366
bool
367
FormNode::isPushbutton()
368
0
{
369
0
    return FT() == "/Btn" && (getFlags() & ff_btn_pushbutton) == ff_btn_pushbutton;
370
0
}
371
372
bool
373
QPDFFormFieldObjectHelper::isChoice()
374
0
{
375
0
    return m->isChoice();
376
0
}
377
378
bool
379
FormNode::isChoice()
380
18.5k
{
381
18.5k
    return FT() == "/Ch";
382
18.5k
}
383
384
std::vector<std::string>
385
QPDFFormFieldObjectHelper::getChoices()
386
0
{
387
0
    return m->getChoices();
388
0
}
389
390
std::vector<std::string>
391
FormNode::getChoices()
392
3.05k
{
393
3.05k
    if (!isChoice()) {
394
0
        return {};
395
0
    }
396
3.05k
    std::vector<std::string> result;
397
79.2k
    for (auto const& item: inheritable_value<Array>("/Opt")) {
398
79.2k
        if (item.isString()) {
399
56.0k
            result.emplace_back(item.getUTF8Value());
400
56.0k
        } else if (item.size() == 2) {
401
402
            auto display = item.getArrayItem(1);
402
402
            if (display.isString()) {
403
247
                result.emplace_back(display.getUTF8Value());
404
247
            }
405
402
        }
406
79.2k
    }
407
3.05k
    return result;
408
3.05k
}
409
410
void
411
QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, QPDFObjectHandle value)
412
0
{
413
0
    m->setFieldAttribute(key, value);
414
0
}
415
416
void
417
FormNode::setFieldAttribute(std::string const& key, QPDFObjectHandle value)
418
0
{
419
0
    replace(key, value);
420
0
}
421
422
void
423
FormNode::setFieldAttribute(std::string const& key, Name const& value)
424
585
{
425
585
    replace(key, value);
426
585
}
427
428
void
429
QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, std::string const& utf8_value)
430
0
{
431
0
    m->setFieldAttribute(key, utf8_value);
432
0
}
433
434
void
435
FormNode::setFieldAttribute(std::string const& key, std::string const& utf8_value)
436
0
{
437
0
    replace(key, String::utf16(utf8_value));
438
0
}
439
440
void
441
QPDFFormFieldObjectHelper::setV(QPDFObjectHandle value, bool need_appearances)
442
1.43k
{
443
1.43k
    m->setV(value, need_appearances);
444
1.43k
}
445
446
void
447
FormNode::setV(QPDFObjectHandle value, bool need_appearances)
448
1.43k
{
449
1.43k
    if (FT() == "/Btn") {
450
1.43k
        Name name = value;
451
1.43k
        if (isCheckbox()) {
452
814
            if (!name) {
453
227
                warn("ignoring attempt to set a checkbox field to a value whose type is not name");
454
227
                return;
455
227
            }
456
            // Accept any value other than /Off to mean checked. Files have been seen that use
457
            // /1 or other values.
458
587
            setCheckBoxValue(name != "/Off");
459
587
            return;
460
814
        }
461
616
        if (isRadioButton()) {
462
614
            if (!name) {
463
77
                warn(
464
77
                    "ignoring attempt to set a radio button field to an object that is not a name");
465
77
                return;
466
77
            }
467
537
            setRadioButtonValue(name);
468
537
            return;
469
614
        }
470
2
        if (isPushbutton()) {
471
0
            warn("ignoring attempt set the value of a pushbutton field");
472
0
        }
473
2
        return;
474
616
    }
475
0
    if (value.isString()) {
476
0
        setFieldAttribute("/V", QPDFObjectHandle::newUnicodeString(value.getUTF8Value()));
477
0
    } else {
478
0
        setFieldAttribute("/V", value);
479
0
    }
480
0
    if (need_appearances) {
481
0
        QPDF& qpdf = oh().getQPDF(
482
0
            "QPDFFormFieldObjectHelper::setV called with need_appearances = "
483
0
            "true on an object that is not associated with an owning QPDF");
484
0
        qpdf.doc().acroform().setNeedAppearances(true);
485
0
    }
486
0
}
487
488
void
489
QPDFFormFieldObjectHelper::setV(std::string const& utf8_value, bool need_appearances)
490
0
{
491
0
    m->setV(utf8_value, need_appearances);
492
0
}
493
494
void
495
FormNode::setV(std::string const& utf8_value, bool need_appearances)
496
0
{
497
0
    setV(QPDFObjectHandle::newUnicodeString(utf8_value), need_appearances);
498
0
}
499
500
void
501
FormNode::setRadioButtonValue(Name const& name)
502
838
{
503
838
    qpdf_expect(name);
504
    // Set the value of a radio button field. This has the following specific behavior:
505
    // * If this is a node without /Kids, assume this is a individual radio button widget and call
506
    // itself on the parent
507
    // * If this is a radio button field with children, set /V to the given value. Then, for each
508
    //   child, if the child has the specified value as one of its keys in the /N subdictionary of
509
    //   its /AP (i.e. its normal appearance stream dictionary), set /AS to name; otherwise, if /Off
510
    //   is a member, set /AS to /Off.
511
838
    auto kids = Kids();
512
838
    if (!kids) {
513
        // This is most likely one of the individual buttons. Try calling on the parent.
514
386
        auto parent = Parent();
515
386
        if (parent.Kids()) {
516
301
            parent.setRadioButtonValue(name);
517
301
            return;
518
301
        }
519
386
    }
520
537
    if (!isRadioButton() || !kids) {
521
134
        warn("don't know how to set the value of this field as a radio button");
522
134
        return;
523
134
    }
524
403
    replace("/V", name);
525
3.91k
    for (FormNode kid: kids) {
526
3.91k
        auto ap = kid.AP();
527
3.91k
        QPDFObjectHandle annot;
528
3.91k
        if (!ap) {
529
            // The widget may be below. If there is more than one, just find the first one.
530
7.32k
            for (FormNode grandkid: kid.Kids()) {
531
7.32k
                ap = grandkid.AP();
532
7.32k
                if (ap) {
533
199
                    annot = grandkid;
534
199
                    break;
535
199
                }
536
7.32k
            }
537
3.22k
        } else {
538
690
            annot = kid;
539
690
        }
540
3.91k
        if (!annot) {
541
3.02k
            warn("unable to set the value of this radio button");
542
3.02k
            continue;
543
3.02k
        }
544
892
        if (ap["/N"].contains(name.value())) {
545
177
            annot.replace("/AS", name);
546
715
        } else {
547
715
            annot.replace("/AS", Name("/Off"));
548
715
        }
549
892
    }
550
403
}
551
552
void
553
FormNode::setCheckBoxValue(bool value)
554
587
{
555
587
    auto ap = AP();
556
587
    QPDFObjectHandle annot;
557
587
    if (ap) {
558
370
        annot = oh();
559
370
    } else {
560
        // The widget may be below. If there is more than one, just find the first one.
561
2.95k
        for (FormNode kid: Kids()) {
562
2.95k
            ap = kid.AP();
563
2.95k
            if (ap) {
564
51
                annot = kid;
565
51
                break;
566
51
            }
567
2.95k
        }
568
217
    }
569
587
    std::string on_value;
570
587
    if (value) {
571
        // Set the "on" value to the first value in the appearance stream's normal state dictionary
572
        // that isn't /Off. If not found, fall back to /Yes.
573
463
        if (ap) {
574
403
            for (auto const& item: Dictionary(ap["/N"])) {
575
403
                if (item.first != "/Off") {
576
234
                    on_value = item.first;
577
234
                    break;
578
234
                }
579
403
            }
580
323
        }
581
463
        if (on_value.empty()) {
582
228
            on_value = "/Yes";
583
228
        }
584
463
    }
585
586
    // Set /AS to the on value or /Off in addition to setting /V.
587
587
    auto name = Name(value ? on_value : "/Off");
588
587
    setFieldAttribute("/V", name);
589
587
    if (!annot) {
590
165
        warn("unable to set the value of this checkbox");
591
165
        return;
592
165
    }
593
422
    annot.replace("/AS", name);
594
422
}
595
596
void
597
QPDFFormFieldObjectHelper::generateAppearance(QPDFAnnotationObjectHelper& aoh)
598
17.3k
{
599
17.3k
    m->generateAppearance(aoh);
600
17.3k
}
601
602
void
603
FormNode::generateAppearance(QPDFAnnotationObjectHelper& aoh)
604
17.3k
{
605
    // Ignore field types we don't know how to generate appearances for. Button fields don't really
606
    // need them -- see code in QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded.
607
17.3k
    auto ft = FT();
608
17.3k
    if (ft == "/Tx" || ft == "/Ch") {
609
15.8k
        generateTextAppearance(aoh);
610
15.8k
    }
611
17.3k
}
612
613
namespace
614
{
615
    class ValueSetter: public QPDFObjectHandle::TokenFilter
616
    {
617
      public:
618
        ValueSetter(
619
            std::string const& DA,
620
            std::string const& V,
621
            std::vector<std::string> const& opt,
622
            double tf,
623
            QPDFObjectHandle::Rectangle const& bbox);
624
15.5k
        ~ValueSetter() override = default;
625
        void handleToken(QPDFTokenizer::Token const&) override;
626
        void handleEOF() override;
627
        void writeAppearance();
628
629
      private:
630
        std::string DA;
631
        std::string V;
632
        std::vector<std::string> opt;
633
        double tf;
634
        QPDFObjectHandle::Rectangle bbox;
635
        enum { st_top, st_bmc, st_emc, st_end } state{st_top};
636
        bool replaced{false};
637
    };
638
} // namespace
639
640
ValueSetter::ValueSetter(
641
    std::string const& DA,
642
    std::string const& V,
643
    std::vector<std::string> const& opt,
644
    double tf,
645
    QPDFObjectHandle::Rectangle const& bbox) :
646
15.5k
    DA(DA),
647
15.5k
    V(V),
648
15.5k
    opt(opt),
649
15.5k
    tf(tf),
650
15.5k
    bbox(bbox)
651
15.5k
{
652
15.5k
}
653
654
void
655
ValueSetter::handleToken(QPDFTokenizer::Token const& token)
656
1.18M
{
657
1.18M
    QPDFTokenizer::token_type_e ttype = token.getType();
658
1.18M
    std::string value = token.getValue();
659
1.18M
    bool do_replace = false;
660
1.18M
    switch (state) {
661
196k
    case st_top:
662
196k
        writeToken(token);
663
196k
        if (token.isWord("BMC")) {
664
12.6k
            state = st_bmc;
665
12.6k
        }
666
196k
        break;
667
668
25.5k
    case st_bmc:
669
25.5k
        if ((ttype == QPDFTokenizer::tt_space) || (ttype == QPDFTokenizer::tt_comment)) {
670
12.8k
            writeToken(token);
671
12.8k
        } else {
672
12.6k
            state = st_emc;
673
12.6k
        }
674
        // fall through to emc
675
676
955k
    case st_emc:
677
955k
        if (token.isWord("EMC")) {
678
12.5k
            do_replace = true;
679
12.5k
            state = st_end;
680
12.5k
        }
681
955k
        break;
682
683
29.7k
    case st_end:
684
29.7k
        writeToken(token);
685
29.7k
        break;
686
1.18M
    }
687
1.18M
    if (do_replace) {
688
12.5k
        writeAppearance();
689
12.5k
    }
690
1.18M
}
691
692
void
693
ValueSetter::handleEOF()
694
14.9k
{
695
14.9k
    if (!replaced) {
696
2.45k
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper replaced BMC at EOF");
697
2.45k
        write("/Tx BMC\n");
698
2.45k
        writeAppearance();
699
2.45k
    }
700
14.9k
}
701
702
void
703
ValueSetter::writeAppearance()
704
14.9k
{
705
14.9k
    replaced = true;
706
707
    // This code does not take quadding into consideration because doing so requires font metric
708
    // information, which we don't have in many cases.
709
710
14.9k
    double tfh = 1.2 * tf;
711
14.9k
    int dx = 1;
712
713
    // Write one or more lines, centered vertically, possibly with one row highlighted.
714
715
14.9k
    auto max_rows = static_cast<size_t>((bbox.ury - bbox.lly) / tfh);
716
14.9k
    bool highlight = false;
717
14.9k
    size_t highlight_idx = 0;
718
719
14.9k
    std::vector<std::string> lines;
720
14.9k
    if (opt.empty() || (max_rows < 2)) {
721
13.8k
        lines.push_back(V);
722
13.8k
    } else {
723
        // Figure out what rows to write
724
1.12k
        size_t nopt = opt.size();
725
1.12k
        size_t found_idx = 0;
726
1.12k
        bool found = false;
727
31.9k
        for (found_idx = 0; found_idx < nopt; ++found_idx) {
728
31.2k
            if (opt.at(found_idx) == V) {
729
423
                found = true;
730
423
                break;
731
423
            }
732
31.2k
        }
733
1.12k
        if (found) {
734
            // Try to make the found item the second one, but adjust for under/overflow.
735
423
            int wanted_first = QIntC::to_int(found_idx) - 1;
736
423
            int wanted_last = QIntC::to_int(found_idx + max_rows) - 2;
737
423
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper list found");
738
423
            if (wanted_first < 0) {
739
142
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper list first too low");
740
142
                wanted_last -= wanted_first;
741
142
                wanted_first = 0;
742
142
            }
743
423
            if (wanted_last >= QIntC::to_int(nopt)) {
744
312
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper list last too high");
745
312
                auto diff = wanted_last - QIntC::to_int(nopt) + 1;
746
312
                wanted_first = std::max(0, wanted_first - diff);
747
312
                wanted_last -= diff;
748
312
            }
749
423
            highlight = true;
750
423
            highlight_idx = found_idx - QIntC::to_size(wanted_first);
751
8.69k
            for (size_t i = QIntC::to_size(wanted_first); i <= QIntC::to_size(wanted_last); ++i) {
752
8.27k
                lines.push_back(opt.at(i));
753
8.27k
            }
754
700
        } else {
755
700
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper list not found");
756
            // include our value and the first n-1 rows
757
700
            highlight_idx = 0;
758
700
            highlight = true;
759
700
            lines.push_back(V);
760
27.7k
            for (size_t i = 0; ((i < nopt) && (i < (max_rows - 1))); ++i) {
761
27.0k
                lines.push_back(opt.at(i));
762
27.0k
            }
763
700
        }
764
1.12k
    }
765
766
    // Write the lines centered vertically, highlighting if needed
767
14.9k
    size_t nlines = lines.size();
768
14.9k
    double dy = bbox.ury - ((bbox.ury - bbox.lly - (static_cast<double>(nlines) * tfh)) / 2.0);
769
14.9k
    if (highlight) {
770
1.09k
        write(
771
1.09k
            "q\n0.85 0.85 0.85 rg\n" + QUtil::double_to_string(bbox.llx) + " " +
772
1.09k
            QUtil::double_to_string(
773
1.09k
                bbox.lly + dy - (tfh * (static_cast<double>(highlight_idx + 1)))) +
774
1.09k
            " " + QUtil::double_to_string(bbox.urx - bbox.llx) + " " +
775
1.09k
            QUtil::double_to_string(tfh) + " re f\nQ\n");
776
1.09k
    }
777
14.9k
    dy -= tf;
778
14.9k
    write("q\nBT\n" + DA + "\n");
779
64.8k
    for (size_t i = 0; i < nlines; ++i) {
780
        // We could adjust Tm to translate to the beginning the first line, set TL to tfh, and use
781
        // T* for each subsequent line, but doing this would require extracting any Tm from DA,
782
        // which doesn't seem really worth the effort.
783
49.8k
        if (i == 0) {
784
14.9k
            write(
785
14.9k
                QUtil::double_to_string(bbox.llx + static_cast<double>(dx)) + " " +
786
14.9k
                QUtil::double_to_string(bbox.lly + static_cast<double>(dy)) + " Td\n");
787
34.9k
        } else {
788
34.9k
            write("0 " + QUtil::double_to_string(-tfh) + " Td\n");
789
34.9k
        }
790
49.8k
        write(QPDFObjectHandle::newString(lines.at(i)).unparse() + " Tj\n");
791
49.8k
    }
792
14.9k
    write("ET\nQ\nEMC");
793
14.9k
}
794
795
namespace
796
{
797
    class TfFinder final: public QPDFObjectHandle::TokenFilter
798
    {
799
      public:
800
15.5k
        TfFinder() = default;
801
15.5k
        ~TfFinder() final = default;
802
803
        void
804
        handleToken(QPDFTokenizer::Token const& token) final
805
2.41M
        {
806
2.41M
            auto ttype = token.getType();
807
2.41M
            auto const& value = token.getValue();
808
2.41M
            DA.emplace_back(token.getRawValue());
809
2.41M
            switch (ttype) {
810
501k
            case QPDFTokenizer::tt_integer:
811
532k
            case QPDFTokenizer::tt_real:
812
532k
                last_num = strtod(value.c_str(), nullptr);
813
532k
                last_num_idx = QIntC::to_int(DA.size() - 1);
814
532k
                break;
815
816
105k
            case QPDFTokenizer::tt_name:
817
105k
                last_name = value;
818
105k
                break;
819
820
113k
            case QPDFTokenizer::tt_word:
821
113k
                if (token.isWord("Tf")) {
822
12.7k
                    if ((last_num > 1.0) && (last_num < 1000.0)) {
823
                        // These ranges are arbitrary but keep us from doing insane things or
824
                        // suffering from over/underflow
825
10.1k
                        tf = last_num;
826
10.1k
                    }
827
12.7k
                    tf_idx = last_num_idx;
828
12.7k
                    font_name = last_name;
829
12.7k
                }
830
113k
                break;
831
832
1.66M
            default:
833
1.66M
                break;
834
2.41M
            }
835
2.41M
        }
836
837
        double
838
        getTf() const
839
15.5k
        {
840
15.5k
            return tf;
841
15.5k
        }
842
        std::string
843
        getFontName() const
844
15.5k
        {
845
15.5k
            return font_name;
846
15.5k
        }
847
848
        std::string
849
        getDA()
850
15.5k
        {
851
15.5k
            std::string result;
852
15.5k
            int i = -1;
853
2.41M
            for (auto const& cur: DA) {
854
2.41M
                if (++i == tf_idx) {
855
10.3k
                    double delta = strtod(cur.c_str(), nullptr) - tf;
856
10.3k
                    if (delta > 0.001 || delta < -0.001) {
857
                        // tf doesn't match the font size passed to Tf, so substitute.
858
1.18k
                        QTC::TC("qpdf", "QPDFFormFieldObjectHelper fallback Tf");
859
1.18k
                        result += QUtil::double_to_string(tf);
860
1.18k
                        continue;
861
1.18k
                    }
862
10.3k
                }
863
2.41M
                result += cur;
864
2.41M
            }
865
15.5k
            return result;
866
15.5k
        }
867
868
      private:
869
        double tf{11.0};
870
        int tf_idx{-1};
871
        std::string font_name;
872
        double last_num{0.0};
873
        int last_num_idx{-1};
874
        std::string last_name;
875
        std::vector<std::string> DA;
876
    };
877
} // namespace
878
879
void
880
FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& aoh)
881
15.8k
{
882
15.8k
    no_ci_warn_if(
883
15.8k
        !Dictionary(aoh), // There is no guarantee that aoh is a dictionary
884
15.8k
        "cannot generate appearance for non-dictionary annotation" //
885
15.8k
    );
886
15.8k
    Stream AS = aoh.getAppearanceStream("/N"); // getAppearanceStream returns a stream or null.
887
15.8k
    if (!AS) {
888
7.85k
        QPDFObjectHandle::Rectangle rect = aoh.getRect(); // may silently be invalid / all zeros
889
7.85k
        QPDFObjectHandle::Rectangle bbox(0, 0, rect.urx - rect.llx, rect.ury - rect.lly);
890
7.85k
        auto* pdf = qpdf();
891
7.85k
        no_ci_stop_damaged_if(!pdf, "unable to get owning QPDF for appearance generation");
892
7.85k
        AS = pdf->newStream("/Tx BMC\nEMC\n");
893
7.85k
        AS.replaceDict(Dictionary(
894
7.85k
            {{"/BBox", QPDFObjectHandle::newFromRectangle(bbox)},
895
7.85k
             {"/Resources", Dictionary({{"/ProcSet", Array({Name("/PDF"), Name("/Text")})}})},
896
7.85k
             {"/Type", Name("/XObject")},
897
7.85k
             {"/Subtype", Name("/Form")}}));
898
7.85k
        if (auto ap = AP()) {
899
1.64k
            ap.replace("/N", AS);
900
6.20k
        } else {
901
6.20k
            aoh.replace("/AP", Dictionary({{"/N", AS}}));
902
6.20k
        }
903
7.85k
    }
904
905
15.8k
    if (AS.obj_sp().use_count() > 3) {
906
        // Ensures that the appearance stream is not shared by copying it if the threshold of 3 is
907
        // exceeded. The threshold is based on the current implementation details:
908
        // - One reference from the local variable AS
909
        // - One reference from the appearance dictionary (/AP)
910
        // - One reference from the object table
911
        // If use_count() is greater than 3, it means the appearance stream is shared elsewhere,
912
        // and updating it could have unintended side effects. This threshold may need to be updated
913
        // if the internal reference counting changes in the future.
914
        //
915
        // There is currently no explicit CI test for this code. It has been manually tested by
916
        // running it through CI with a threshold of 0, unconditionally copying streams.
917
477
        auto data = AS.getStreamData(qpdf_dl_all);
918
477
        AS = AS.copy();
919
477
        AS.replaceStreamData(std::move(data), Null::temp(), Null::temp());
920
477
        if (Dictionary AP = aoh.getAppearanceDictionary()) {
921
435
            AP.replace("/N", AS);
922
435
        } else {
923
42
            aoh.replace("/AP", Dictionary({{"/N", AS}}));
924
            // aoh is a dictionary, so insertion will succeed. No need to check by retrieving it.
925
42
        }
926
477
    }
927
15.8k
    QPDFObjectHandle bbox_obj = AS.getDict()["/BBox"];
928
15.8k
    if (!bbox_obj.isRectangle()) {
929
224
        aoh.warn("unable to get appearance stream bounding box");
930
224
        return;
931
224
    }
932
15.5k
    QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle();
933
15.5k
    std::string DA = default_appearance();
934
15.5k
    std::string V = value();
935
936
15.5k
    TfFinder tff;
937
15.5k
    Pl_QPDFTokenizer tok("tf", &tff);
938
15.5k
    tok.writeString(DA);
939
15.5k
    tok.finish();
940
15.5k
    double tf = tff.getTf();
941
15.5k
    DA = tff.getDA();
942
943
15.5k
    std::string (*encoder)(std::string const&, char) = &QUtil::utf8_to_ascii;
944
15.5k
    std::string font_name = tff.getFontName();
945
15.5k
    if (!font_name.empty()) {
946
        // See if the font is encoded with something we know about.
947
10.3k
        Dictionary resources = AS.getDict()["/Resources"];
948
10.3k
        Dictionary font = resources["/Font"][font_name];
949
10.3k
        if (!font) {
950
10.1k
            font = getDefaultResources()["/Font"][font_name];
951
10.1k
            if (resources) {
952
7.22k
                if (resources.indirect()) {
953
424
                    resources = resources.qpdf()->makeIndirectObject(resources.copy());
954
424
                    AS.getDict().replace("/Resources", resources);
955
424
                }
956
                // Use mergeResources to force /Font to be local
957
7.22k
                QPDFObjectHandle res = resources;
958
7.22k
                res.mergeResources(Dictionary({{"/Font", Dictionary::empty()}}));
959
7.22k
                res.getKey("/Font").replace(font_name, font);
960
7.22k
            }
961
10.1k
        }
962
963
10.3k
        if (Name Encoding = font["/Encoding"]) {
964
886
            if (Encoding == "/WinAnsiEncoding") {
965
381
                encoder = &QUtil::utf8_to_win_ansi;
966
505
            } else if (Encoding == "/MacRomanEncoding") {
967
95
                encoder = &QUtil::utf8_to_mac_roman;
968
95
            }
969
886
        }
970
10.3k
    }
971
972
15.5k
    V = (*encoder)(V, '?');
973
974
15.5k
    std::vector<std::string> opt;
975
15.5k
    if (isChoice() && (getFlags() & ff_ch_combo) == 0) {
976
3.05k
        opt = getChoices();
977
56.3k
        for (auto& o: opt) {
978
56.3k
            o = (*encoder)(o, '?');
979
56.3k
        }
980
3.05k
    }
981
982
15.5k
    std::string result;
983
15.5k
    pl::String pl(result);
984
15.5k
    ValueSetter vs(DA, V, opt, tf, bbox);
985
15.5k
    Pl_QPDFTokenizer vs_tok("", &vs, &pl);
986
15.5k
    vs_tok.writeString(AS.getStreamData(qpdf_dl_all));
987
15.5k
    vs_tok.finish();
988
15.5k
    AS.replaceStreamData(std::move(result), Null::temp(), Null::temp());
989
15.5k
}