Coverage Report

Created: 2025-12-05 06:58

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/Pl_QPDFTokenizer.hh>
6
#include <qpdf/QIntC.hh>
7
#include <qpdf/QPDFAcroFormDocumentHelper.hh>
8
#include <qpdf/QPDFAnnotationObjectHelper.hh>
9
#include <qpdf/QPDFObjectHandle_private.hh>
10
#include <qpdf/QPDF_private.hh>
11
#include <qpdf/QTC.hh>
12
#include <qpdf/QUtil.hh>
13
#include <cstdlib>
14
15
#include <memory>
16
17
using namespace qpdf;
18
19
using FormNode = qpdf::impl::FormNode;
20
21
const QPDFObjectHandle FormNode::null_oh;
22
23
class QPDFFormFieldObjectHelper::Members: public FormNode
24
{
25
  public:
26
    Members(QPDFObjectHandle const& oh) :
27
114k
        FormNode(oh)
28
114k
    {
29
114k
    }
30
};
31
32
QPDFFormFieldObjectHelper::QPDFFormFieldObjectHelper(QPDFObjectHandle o) :
33
71.3k
    QPDFObjectHelper(o),
34
71.3k
    m(std::make_shared<Members>(oh()))
35
71.3k
{
36
71.3k
}
37
38
QPDFFormFieldObjectHelper::QPDFFormFieldObjectHelper() :
39
42.9k
    QPDFObjectHelper(Null::temp()),
40
42.9k
    m(std::make_shared<Members>(QPDFObjectHandle()))
41
42.9k
{
42
42.9k
}
43
44
bool
45
QPDFFormFieldObjectHelper::isNull()
46
0
{
47
0
    return m->null();
48
0
}
49
50
QPDFFormFieldObjectHelper
51
QPDFFormFieldObjectHelper::getParent()
52
0
{
53
0
    return {Null::if_null(m->Parent().oh())};
54
0
}
55
56
QPDFFormFieldObjectHelper
57
QPDFFormFieldObjectHelper::getTopLevelField(bool* is_different)
58
0
{
59
0
    return Null::if_null(m->root_field(is_different).oh());
60
0
}
61
62
FormNode
63
FormNode::root_field(bool* is_different)
64
0
{
65
0
    if (is_different) {
66
0
        *is_different = false;
67
0
    }
68
0
    if (!obj) {
69
0
        return {};
70
0
    }
71
0
    auto rf = *this;
72
0
    size_t depth = 0; // Don't bother with loop detection until depth becomes suspicious
73
0
    QPDFObjGen::set seen;
74
0
    while (rf.Parent() && (++depth < 10 || seen.add(rf))) {
75
0
        rf = rf.Parent();
76
0
        if (is_different) {
77
0
            *is_different = true;
78
0
        }
79
0
    }
80
0
    return rf;
81
0
}
82
83
QPDFObjectHandle
84
QPDFFormFieldObjectHelper::getInheritableFieldValue(std::string const& name)
85
0
{
86
0
    return Null::if_null(m->inheritable_value<QPDFObjectHandle>(name));
87
0
}
88
89
QPDFObjectHandle const&
90
FormNode::inherited(std::string const& name, bool acroform) const
91
25.6k
{
92
25.6k
    if (!obj) {
93
1.10k
        return null_oh;
94
1.10k
    }
95
24.5k
    auto node = *this;
96
24.5k
    QPDFObjGen::set seen;
97
24.5k
    size_t depth = 0; // Don't bother with loop detection until depth becomes suspicious
98
38.2k
    while (node.Parent() && (++depth < 10 || seen.add(node))) {
99
18.0k
        node = node.Parent();
100
18.0k
        if (auto const& result = node[name]) {
101
4.33k
            return {result};
102
4.33k
        }
103
18.0k
    }
104
20.1k
    return acroform ? from_AcroForm(name) : null_oh;
105
24.5k
}
106
107
std::string
108
QPDFFormFieldObjectHelper::getInheritableFieldValueAsString(std::string const& name)
109
0
{
110
0
    return m->inheritable_string(name);
111
0
}
112
113
std::string
114
FormNode::inheritable_string(std::string const& name) const
115
12.3k
{
116
12.3k
    if (auto fv = inheritable_value<String>(name)) {
117
2.42k
        return fv.utf8_value();
118
2.42k
    }
119
9.95k
    return {};
120
12.3k
}
121
122
std::string
123
QPDFFormFieldObjectHelper::getInheritableFieldValueAsName(std::string const& name)
124
0
{
125
0
    if (auto fv = m->inheritable_value<Name>(name)) {
126
0
        return fv;
127
0
    }
128
0
    return {};
129
0
}
130
131
std::string
132
QPDFFormFieldObjectHelper::getFieldType()
133
15.9k
{
134
15.9k
    if (auto ft = m->FT()) {
135
15.0k
        return ft;
136
15.0k
    }
137
987
    return {};
138
15.9k
}
139
140
std::string
141
QPDFFormFieldObjectHelper::getFullyQualifiedName()
142
21.3k
{
143
21.3k
    return m->fully_qualified_name();
144
21.3k
}
145
146
std::string
147
FormNode::fully_qualified_name() const
148
21.3k
{
149
21.3k
    std::string result;
150
21.3k
    auto node = *this;
151
21.3k
    QPDFObjGen::set seen;
152
21.3k
    size_t depth = 0; // Don't bother with loop detection until depth becomes suspicious
153
91.2k
    while (node && (++depth < 10 || seen.add(node))) {
154
69.8k
        if (auto T = node.T()) {
155
65.8k
            if (!result.empty()) {
156
44.4k
                result.insert(0, 1, '.');
157
44.4k
            }
158
65.8k
            result.insert(0, T.utf8_value());
159
65.8k
        }
160
69.8k
        node = node.Parent();
161
69.8k
    }
162
21.3k
    return result;
163
21.3k
}
164
165
std::string
166
QPDFFormFieldObjectHelper::getPartialName()
167
0
{
168
0
    return m->partial_name();
169
0
}
170
171
std::string
172
FormNode::partial_name() const
173
0
{
174
0
    if (auto pn = T()) {
175
0
        return pn.utf8_value();
176
0
    }
177
0
    return {};
178
0
}
179
180
std::string
181
QPDFFormFieldObjectHelper::getAlternativeName()
182
0
{
183
0
    return m->alternative_name();
184
0
}
185
186
std::string
187
FormNode::alternative_name() const
188
0
{
189
0
    if (auto an = TU()) {
190
0
        return an.utf8_value();
191
0
    }
192
0
    return fully_qualified_name();
193
0
}
194
195
std::string
196
QPDFFormFieldObjectHelper::getMappingName()
197
0
{
198
0
    return m->mapping_name();
199
0
}
200
201
std::string
202
FormNode::mapping_name() const
203
0
{
204
0
    if (auto mn = TM()) {
205
0
        return mn.utf8_value();
206
0
    }
207
0
    return alternative_name();
208
0
}
209
210
QPDFObjectHandle
211
QPDFFormFieldObjectHelper::getValue()
212
2.02k
{
213
2.02k
    return Null::if_null(m->V<QPDFObjectHandle>());
214
2.02k
}
215
216
std::string
217
QPDFFormFieldObjectHelper::getValueAsString()
218
0
{
219
0
    return m->value();
220
0
}
221
222
std::string
223
FormNode::value() const
224
12.3k
{
225
12.3k
    return inheritable_string("/V");
226
12.3k
}
227
228
QPDFObjectHandle
229
QPDFFormFieldObjectHelper::getDefaultValue()
230
0
{
231
0
    return Null::if_null(m->DV());
232
0
}
233
234
std::string
235
QPDFFormFieldObjectHelper::getDefaultValueAsString()
236
0
{
237
0
    return m->default_value();
238
0
}
239
240
std::string
241
FormNode::default_value() const
242
0
{
243
0
    return inheritable_string("/DV");
244
0
}
245
246
QPDFObjectHandle
247
QPDFFormFieldObjectHelper::getDefaultResources()
248
18.1k
{
249
18.1k
    return Null::if_null(m->getDefaultResources());
250
18.1k
}
251
252
QPDFObjectHandle
253
FormNode::getDefaultResources()
254
26.5k
{
255
26.5k
    return from_AcroForm("/DR");
256
26.5k
}
257
258
std::string
259
QPDFFormFieldObjectHelper::getDefaultAppearance()
260
0
{
261
0
    return m->default_appearance();
262
0
}
263
264
std::string
265
FormNode::default_appearance() const
266
12.3k
{
267
12.3k
    if (auto DA = inheritable_value<String>("/DA")) {
268
9.65k
        return DA.utf8_value();
269
9.65k
    }
270
2.72k
    if (String DA = from_AcroForm("/DA")) {
271
72
        return DA.utf8_value();
272
72
    }
273
2.64k
    return {};
274
2.72k
}
275
276
int
277
QPDFFormFieldObjectHelper::getQuadding()
278
0
{
279
0
    return m->getQuadding();
280
0
}
281
282
int
283
FormNode::getQuadding()
284
0
{
285
0
    auto fv = inheritable_value<QPDFObjectHandle>("/Q");
286
0
    bool looked_in_acroform = false;
287
0
    if (!fv.isInteger()) {
288
0
        fv = from_AcroForm("/Q");
289
0
        looked_in_acroform = true;
290
0
    }
291
0
    if (fv.isInteger()) {
292
0
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper Q present", looked_in_acroform ? 0 : 1);
293
0
        return QIntC::to_int(fv.getIntValue());
294
0
    }
295
0
    return 0;
296
0
}
297
298
int
299
QPDFFormFieldObjectHelper::getFlags()
300
0
{
301
0
    return m->getFlags();
302
0
}
303
304
int
305
FormNode::getFlags()
306
10.4k
{
307
10.4k
    auto f = inheritable_value<QPDFObjectHandle>("/Ff");
308
10.4k
    return f.isInteger() ? f.getIntValueAsInt() : 0;
309
10.4k
}
310
311
bool
312
QPDFFormFieldObjectHelper::isText()
313
0
{
314
0
    return m->isText();
315
0
}
316
317
bool
318
FormNode::isText()
319
0
{
320
0
    return FT() == "/Tx";
321
0
}
322
323
bool
324
QPDFFormFieldObjectHelper::isCheckbox()
325
1.08k
{
326
1.08k
    return m->isCheckbox();
327
1.08k
}
328
329
bool
330
FormNode::isCheckbox()
331
3.11k
{
332
3.11k
    return FT() == "/Btn" && (getFlags() & (ff_btn_radio | ff_btn_pushbutton)) == 0;
333
3.11k
}
334
335
bool
336
QPDFFormFieldObjectHelper::isChecked()
337
0
{
338
0
    return m->isChecked();
339
0
}
340
341
bool
342
FormNode::isChecked()
343
0
{
344
0
    return isCheckbox() && V<Name>() != "/Off";
345
0
}
346
347
bool
348
QPDFFormFieldObjectHelper::isRadioButton()
349
2.07k
{
350
2.07k
    return m->isRadioButton();
351
2.07k
}
352
353
bool
354
FormNode::isRadioButton()
355
4.37k
{
356
4.37k
    return FT() == "/Btn" && (getFlags() & ff_btn_radio) == ff_btn_radio;
357
4.37k
}
358
359
bool
360
QPDFFormFieldObjectHelper::isPushbutton()
361
0
{
362
0
    return m->isPushbutton();
363
0
}
364
365
bool
366
FormNode::isPushbutton()
367
0
{
368
0
    return FT() == "/Btn" && (getFlags() & ff_btn_pushbutton) == ff_btn_pushbutton;
369
0
}
370
371
bool
372
QPDFFormFieldObjectHelper::isChoice()
373
0
{
374
0
    return m->isChoice();
375
0
}
376
377
bool
378
FormNode::isChoice()
379
14.8k
{
380
14.8k
    return FT() == "/Ch";
381
14.8k
}
382
383
std::vector<std::string>
384
QPDFFormFieldObjectHelper::getChoices()
385
0
{
386
0
    return m->getChoices();
387
0
}
388
389
std::vector<std::string>
390
FormNode::getChoices()
391
2.43k
{
392
2.43k
    if (!isChoice()) {
393
0
        return {};
394
0
    }
395
2.43k
    std::vector<std::string> result;
396
32.4k
    for (auto const& item: inheritable_value<Array>("/Opt")) {
397
32.4k
        if (item.isString()) {
398
18.8k
            result.emplace_back(item.getUTF8Value());
399
18.8k
        } else if (item.size() == 2) {
400
242
            auto display = item.getArrayItem(1);
401
242
            if (display.isString()) {
402
125
                result.emplace_back(display.getUTF8Value());
403
125
            }
404
242
        }
405
32.4k
    }
406
2.43k
    return result;
407
2.43k
}
408
409
void
410
QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, QPDFObjectHandle value)
411
0
{
412
0
    m->setFieldAttribute(key, value);
413
0
}
414
415
void
416
FormNode::setFieldAttribute(std::string const& key, QPDFObjectHandle value)
417
1.23k
{
418
1.23k
    oh().replaceKey(key, value);
419
1.23k
}
420
421
void
422
QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, std::string const& utf8_value)
423
0
{
424
0
    m->setFieldAttribute(key, utf8_value);
425
0
}
426
427
void
428
FormNode::setFieldAttribute(std::string const& key, std::string const& utf8_value)
429
0
{
430
0
    oh().replaceKey(key, QPDFObjectHandle::newUnicodeString(utf8_value));
431
0
}
432
433
void
434
QPDFFormFieldObjectHelper::setV(QPDFObjectHandle value, bool need_appearances)
435
2.02k
{
436
2.02k
    m->setV(value, need_appearances);
437
2.02k
}
438
439
void
440
FormNode::setV(QPDFObjectHandle value, bool need_appearances)
441
2.02k
{
442
2.02k
    Name name = value;
443
2.02k
    if (FT() == "/Btn") {
444
2.02k
        if (isCheckbox()) {
445
1.04k
            if (!name) {
446
340
                warn("ignoring attempt to set a checkbox field to a value whose type is not name");
447
340
                return;
448
340
            }
449
            // Accept any value other than /Off to mean checked. Files have been seen that use
450
            // /1 or other values.
451
704
            setCheckBoxValue(name != "/Off");
452
704
            return;
453
1.04k
        }
454
984
        if (isRadioButton()) {
455
983
            if (!name) {
456
184
                warn(
457
184
                    "ignoring attempt to set a radio button field to an object that is not a name");
458
184
                return;
459
184
            }
460
799
            setRadioButtonValue(name);
461
799
            return;
462
983
        }
463
1
        if (isPushbutton()) {
464
0
            warn("ignoring attempt set the value of a pushbutton field");
465
0
        }
466
1
        return;
467
984
    }
468
0
    if (value.isString()) {
469
0
        setFieldAttribute("/V", QPDFObjectHandle::newUnicodeString(value.getUTF8Value()));
470
0
    } else {
471
0
        setFieldAttribute("/V", value);
472
0
    }
473
0
    if (need_appearances) {
474
0
        QPDF& qpdf = oh().getQPDF(
475
0
            "QPDFFormFieldObjectHelper::setV called with need_appearances = "
476
0
            "true on an object that is not associated with an owning QPDF");
477
0
        qpdf.doc().acroform().setNeedAppearances(true);
478
0
    }
479
0
}
480
481
void
482
QPDFFormFieldObjectHelper::setV(std::string const& utf8_value, bool need_appearances)
483
0
{
484
0
    m->setV(utf8_value, need_appearances);
485
0
}
486
487
void
488
FormNode::setV(std::string const& utf8_value, bool need_appearances)
489
0
{
490
0
    setV(QPDFObjectHandle::newUnicodeString(utf8_value), need_appearances);
491
0
}
492
493
void
494
FormNode::setRadioButtonValue(QPDFObjectHandle name)
495
1.21k
{
496
    // Set the value of a radio button field. This has the following specific behavior:
497
    // * If this is a radio button field that has a parent that is also a radio button field and has
498
    //   no explicit /V, call itself on the parent
499
    // * If this is a radio button field with children, set /V to the given value. Then, for each
500
    //   child, if the child has the specified value as one of its keys in the /N subdictionary of
501
    //   its /AP (i.e. its normal appearance stream dictionary), set /AS to name; otherwise, if /Off
502
    //   is a member, set /AS to /Off.
503
    // Note that we never turn on /NeedAppearances when setting a radio button field.
504
1.21k
    QPDFObjectHandle parent = oh().getKey("/Parent");
505
1.21k
    if (parent.isDictionary() && parent.getKey("/Parent").null()) {
506
521
        FormNode ph(parent);
507
521
        if (ph.isRadioButton()) {
508
            // This is most likely one of the individual buttons. Try calling on the parent.
509
413
            ph.setRadioButtonValue(name);
510
413
            return;
511
413
        }
512
521
    }
513
514
799
    QPDFObjectHandle kids = oh().getKey("/Kids");
515
799
    if (!(isRadioButton() && parent.null() && kids.isArray())) {
516
219
        warn("don't know how to set the value of this field as a radio button");
517
219
        return;
518
219
    }
519
580
    setFieldAttribute("/V", name);
520
2.12k
    for (auto const& kid: kids.as_array()) {
521
2.12k
        QPDFObjectHandle AP = kid.getKey("/AP");
522
2.12k
        QPDFObjectHandle annot;
523
2.12k
        if (AP.null()) {
524
            // The widget may be below. If there is more than one, just find the first one.
525
1.05k
            for (auto const& grandkid: kid.getKey("/Kids").as_array()) {
526
1.05k
                AP = grandkid.getKey("/AP");
527
1.05k
                if (!AP.null()) {
528
99
                    annot = grandkid;
529
99
                    break;
530
99
                }
531
1.05k
            }
532
1.07k
        } else {
533
1.07k
            annot = kid;
534
1.07k
        }
535
2.12k
        if (!annot) {
536
925
            warn("unable to set the value of this radio button");
537
925
            continue;
538
925
        }
539
1.19k
        if (AP.isDictionary() && AP.getKey("/N").isDictionary() &&
540
848
            AP.getKey("/N").hasKey(name.getName())) {
541
128
            annot.replaceKey("/AS", name);
542
1.07k
        } else {
543
1.07k
            annot.replaceKey("/AS", QPDFObjectHandle::newName("/Off"));
544
1.07k
        }
545
1.19k
    }
546
580
}
547
548
void
549
FormNode::setCheckBoxValue(bool value)
550
704
{
551
704
    QPDFObjectHandle AP = oh().getKey("/AP");
552
704
    QPDFObjectHandle annot;
553
704
    if (AP.null()) {
554
        // The widget may be below. If there is more than one, just
555
        // find the first one.
556
245
        QPDFObjectHandle kids = oh().getKey("/Kids");
557
841
        for (auto const& kid: oh().getKey("/Kids").as_array(qpdf::strict)) {
558
841
            AP = kid.getKey("/AP");
559
841
            if (!AP.null()) {
560
104
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper checkbox kid widget");
561
104
                annot = kid;
562
104
                break;
563
104
            }
564
841
        }
565
459
    } else {
566
459
        annot = oh();
567
459
    }
568
704
    std::string on_value;
569
704
    if (value) {
570
        // Set the "on" value to the first value in the appearance stream's normal state dictionary
571
        // that isn't /Off. If not found, fall back to /Yes.
572
454
        if (AP.isDictionary()) {
573
481
            for (auto const& item: AP.getKey("/N").as_dictionary()) {
574
481
                if (item.first != "/Off") {
575
279
                    on_value = item.first;
576
279
                    break;
577
279
                }
578
481
            }
579
362
        }
580
454
        if (on_value.empty()) {
581
173
            on_value = "/Yes";
582
173
        }
583
454
    }
584
585
    // Set /AS to the on value or /Off in addition to setting /V.
586
704
    QPDFObjectHandle name = QPDFObjectHandle::newName(value ? on_value : "/Off");
587
704
    setFieldAttribute("/V", name);
588
704
    if (!annot) {
589
96
        QTC::TC("qpdf", "QPDFObjectHandle broken checkbox");
590
96
        warn("unable to set the value of this checkbox");
591
96
        return;
592
96
    }
593
608
    QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS");
594
608
    annot.replaceKey("/AS", name);
595
608
}
596
597
void
598
QPDFFormFieldObjectHelper::generateAppearance(QPDFAnnotationObjectHelper& aoh)
599
13.9k
{
600
13.9k
    m->generateAppearance(aoh);
601
13.9k
}
602
603
void
604
FormNode::generateAppearance(QPDFAnnotationObjectHelper& aoh)
605
13.9k
{
606
    // Ignore field types we don't know how to generate appearances for. Button fields don't really
607
    // need them -- see code in QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded.
608
13.9k
    if (FT() == "/Tx" || FT() == "/Ch") {
609
12.6k
        generateTextAppearance(aoh);
610
12.6k
    }
611
13.9k
}
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
12.3k
        ~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
12.3k
    DA(DA),
647
12.3k
    V(V),
648
12.3k
    opt(opt),
649
12.3k
    tf(tf),
650
12.3k
    bbox(bbox)
651
12.3k
{
652
12.3k
}
653
654
void
655
ValueSetter::handleToken(QPDFTokenizer::Token const& token)
656
0
{
657
0
    QPDFTokenizer::token_type_e ttype = token.getType();
658
0
    std::string value = token.getValue();
659
0
    bool do_replace = false;
660
0
    switch (state) {
661
0
    case st_top:
662
0
        writeToken(token);
663
0
        if (token.isWord("BMC")) {
664
0
            state = st_bmc;
665
0
        }
666
0
        break;
667
668
0
    case st_bmc:
669
0
        if ((ttype == QPDFTokenizer::tt_space) || (ttype == QPDFTokenizer::tt_comment)) {
670
0
            writeToken(token);
671
0
        } else {
672
0
            state = st_emc;
673
0
        }
674
        // fall through to emc
675
676
0
    case st_emc:
677
0
        if (token.isWord("EMC")) {
678
0
            do_replace = true;
679
0
            state = st_end;
680
0
        }
681
0
        break;
682
683
0
    case st_end:
684
0
        writeToken(token);
685
0
        break;
686
0
    }
687
0
    if (do_replace) {
688
0
        writeAppearance();
689
0
    }
690
0
}
691
692
void
693
ValueSetter::handleEOF()
694
0
{
695
0
    if (!replaced) {
696
0
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper replaced BMC at EOF");
697
0
        write("/Tx BMC\n");
698
0
        writeAppearance();
699
0
    }
700
0
}
701
702
void
703
ValueSetter::writeAppearance()
704
0
{
705
0
    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
0
    double tfh = 1.2 * tf;
711
0
    int dx = 1;
712
713
    // Write one or more lines, centered vertically, possibly with one row highlighted.
714
715
0
    auto max_rows = static_cast<size_t>((bbox.ury - bbox.lly) / tfh);
716
0
    bool highlight = false;
717
0
    size_t highlight_idx = 0;
718
719
0
    std::vector<std::string> lines;
720
0
    if (opt.empty() || (max_rows < 2)) {
721
0
        lines.push_back(V);
722
0
    } else {
723
        // Figure out what rows to write
724
0
        size_t nopt = opt.size();
725
0
        size_t found_idx = 0;
726
0
        bool found = false;
727
0
        for (found_idx = 0; found_idx < nopt; ++found_idx) {
728
0
            if (opt.at(found_idx) == V) {
729
0
                found = true;
730
0
                break;
731
0
            }
732
0
        }
733
0
        if (found) {
734
            // Try to make the found item the second one, but adjust for under/overflow.
735
0
            int wanted_first = QIntC::to_int(found_idx) - 1;
736
0
            int wanted_last = QIntC::to_int(found_idx + max_rows) - 2;
737
0
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper list found");
738
0
            if (wanted_first < 0) {
739
0
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper list first too low");
740
0
                wanted_last -= wanted_first;
741
0
                wanted_first = 0;
742
0
            }
743
0
            if (wanted_last >= QIntC::to_int(nopt)) {
744
0
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper list last too high");
745
0
                auto diff = wanted_last - QIntC::to_int(nopt) + 1;
746
0
                wanted_first = std::max(0, wanted_first - diff);
747
0
                wanted_last -= diff;
748
0
            }
749
0
            highlight = true;
750
0
            highlight_idx = found_idx - QIntC::to_size(wanted_first);
751
0
            for (size_t i = QIntC::to_size(wanted_first); i <= QIntC::to_size(wanted_last); ++i) {
752
0
                lines.push_back(opt.at(i));
753
0
            }
754
0
        } else {
755
0
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper list not found");
756
            // include our value and the first n-1 rows
757
0
            highlight_idx = 0;
758
0
            highlight = true;
759
0
            lines.push_back(V);
760
0
            for (size_t i = 0; ((i < nopt) && (i < (max_rows - 1))); ++i) {
761
0
                lines.push_back(opt.at(i));
762
0
            }
763
0
        }
764
0
    }
765
766
    // Write the lines centered vertically, highlighting if needed
767
0
    size_t nlines = lines.size();
768
0
    double dy = bbox.ury - ((bbox.ury - bbox.lly - (static_cast<double>(nlines) * tfh)) / 2.0);
769
0
    if (highlight) {
770
0
        write(
771
0
            "q\n0.85 0.85 0.85 rg\n" + QUtil::double_to_string(bbox.llx) + " " +
772
0
            QUtil::double_to_string(
773
0
                bbox.lly + dy - (tfh * (static_cast<double>(highlight_idx + 1)))) +
774
0
            " " + QUtil::double_to_string(bbox.urx - bbox.llx) + " " +
775
0
            QUtil::double_to_string(tfh) + " re f\nQ\n");
776
0
    }
777
0
    dy -= tf;
778
0
    write("q\nBT\n" + DA + "\n");
779
0
    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
0
        if (i == 0) {
784
0
            write(
785
0
                QUtil::double_to_string(bbox.llx + static_cast<double>(dx)) + " " +
786
0
                QUtil::double_to_string(bbox.lly + static_cast<double>(dy)) + " Td\n");
787
0
        } else {
788
0
            write("0 " + QUtil::double_to_string(-tfh) + " Td\n");
789
0
        }
790
0
        write(QPDFObjectHandle::newString(lines.at(i)).unparse() + " Tj\n");
791
0
    }
792
0
    write("ET\nQ\nEMC");
793
0
}
794
795
namespace
796
{
797
    class TfFinder final: public QPDFObjectHandle::TokenFilter
798
    {
799
      public:
800
12.3k
        TfFinder() = default;
801
12.3k
        ~TfFinder() final = default;
802
803
        void
804
        handleToken(QPDFTokenizer::Token const& token) final
805
4.47M
        {
806
4.47M
            auto ttype = token.getType();
807
4.47M
            auto const& value = token.getValue();
808
4.47M
            DA.emplace_back(token.getRawValue());
809
4.47M
            switch (ttype) {
810
110k
            case QPDFTokenizer::tt_integer:
811
141k
            case QPDFTokenizer::tt_real:
812
141k
                last_num = strtod(value.c_str(), nullptr);
813
141k
                last_num_idx = QIntC::to_int(DA.size() - 1);
814
141k
                break;
815
816
1.44M
            case QPDFTokenizer::tt_name:
817
1.44M
                last_name = value;
818
1.44M
                break;
819
820
117k
            case QPDFTokenizer::tt_word:
821
117k
                if (token.isWord("Tf")) {
822
12.8k
                    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
9.02k
                        tf = last_num;
826
9.02k
                    }
827
12.8k
                    tf_idx = last_num_idx;
828
12.8k
                    font_name = last_name;
829
12.8k
                }
830
117k
                break;
831
832
2.76M
            default:
833
2.76M
                break;
834
4.47M
            }
835
4.47M
        }
836
837
        double
838
        getTf() const
839
12.3k
        {
840
12.3k
            return tf;
841
12.3k
        }
842
        std::string
843
        getFontName() const
844
12.3k
        {
845
12.3k
            return font_name;
846
12.3k
        }
847
848
        std::string
849
        getDA()
850
12.3k
        {
851
12.3k
            std::string result;
852
12.3k
            int i = -1;
853
4.47M
            for (auto const& cur: DA) {
854
4.47M
                if (++i == tf_idx) {
855
8.65k
                    double delta = strtod(cur.c_str(), nullptr) - tf;
856
8.65k
                    if (delta > 0.001 || delta < -0.001) {
857
                        // tf doesn't match the font size passed to Tf, so substitute.
858
1.11k
                        QTC::TC("qpdf", "QPDFFormFieldObjectHelper fallback Tf");
859
1.11k
                        result += QUtil::double_to_string(tf);
860
1.11k
                        continue;
861
1.11k
                    }
862
8.65k
                }
863
4.47M
                result += cur;
864
4.47M
            }
865
12.3k
            return result;
866
12.3k
        }
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
QPDFObjectHandle
880
FormNode::getFontFromResource(QPDFObjectHandle resources, std::string const& name)
881
17.0k
{
882
17.0k
    QPDFObjectHandle result;
883
17.0k
    if (resources.isDictionary() && resources.getKey("/Font").isDictionary() &&
884
4.21k
        resources.getKey("/Font").hasKey(name)) {
885
1.05k
        result = resources.getKey("/Font").getKey(name);
886
1.05k
    }
887
17.0k
    return result;
888
17.0k
}
889
890
void
891
FormNode::generateTextAppearance(QPDFAnnotationObjectHelper& aoh)
892
12.6k
{
893
12.6k
    QPDFObjectHandle AS = aoh.getAppearanceStream("/N");
894
12.6k
    if (AS.null()) {
895
8.71k
        QPDFObjectHandle::Rectangle rect = aoh.getRect();
896
8.71k
        QPDFObjectHandle::Rectangle bbox(0, 0, rect.urx - rect.llx, rect.ury - rect.lly);
897
8.71k
        auto dict = Dictionary(
898
8.71k
            {{"/BBox", QPDFObjectHandle::newFromRectangle(bbox)},
899
8.71k
             {"/Resources", Dictionary({{"/ProcSet", Array({Name("/PDF"), Name("/Text")})}})},
900
8.71k
             {"/Type", Name("/XObject")},
901
8.71k
             {"/Subtype", Name("/Form")}});
902
8.71k
        AS = QPDFObjectHandle::newStream(oh().getOwningQPDF(), "/Tx BMC\nEMC\n");
903
8.71k
        AS.replaceDict(dict);
904
8.71k
        Dictionary AP = aoh.getAppearanceDictionary();
905
8.71k
        if (!AP) {
906
6.53k
            aoh.getObjectHandle().replaceKey("/AP", Dictionary::empty());
907
6.53k
            AP = aoh.getAppearanceDictionary();
908
6.53k
        }
909
8.71k
        AP.replace("/N", AS);
910
8.71k
    }
911
12.6k
    if (!AS.isStream()) {
912
0
        aoh.warn("unable to get normal appearance stream for update");
913
0
        return;
914
0
    }
915
916
12.6k
    if (AS.obj_sp().use_count() > 3) {
917
95
        aoh.warn("unable to generate text appearance from shared appearance stream for update");
918
95
        return;
919
95
    }
920
12.5k
    QPDFObjectHandle bbox_obj = AS.getDict().getKey("/BBox");
921
12.5k
    if (!bbox_obj.isRectangle()) {
922
178
        aoh.warn("unable to get appearance stream bounding box");
923
178
        return;
924
178
    }
925
12.4k
    QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle();
926
12.4k
    std::string DA = default_appearance();
927
12.4k
    std::string V = value();
928
12.4k
    std::vector<std::string> opt;
929
12.4k
    if (isChoice() && (getFlags() & ff_ch_combo) == 0) {
930
2.43k
        opt = getChoices();
931
2.43k
    }
932
933
12.4k
    TfFinder tff;
934
12.4k
    Pl_QPDFTokenizer tok("tf", &tff);
935
12.4k
    tok.writeString(DA);
936
12.4k
    tok.finish();
937
12.4k
    double tf = tff.getTf();
938
12.4k
    DA = tff.getDA();
939
940
12.4k
    std::string (*encoder)(std::string const&, char) = &QUtil::utf8_to_ascii;
941
12.4k
    std::string font_name = tff.getFontName();
942
12.4k
    if (!font_name.empty()) {
943
        // See if the font is encoded with something we know about.
944
8.62k
        Dictionary resources = AS.getDict()["/Resources"];
945
8.62k
        Dictionary font = getFontFromResource(resources, font_name);
946
8.62k
        if (!font) {
947
8.41k
            font = getFontFromResource(getDefaultResources(), font_name);
948
8.41k
            if (resources) {
949
7.29k
                if (resources.indirect()) {
950
420
                    resources = resources.qpdf()->makeIndirectObject(resources.copy());
951
420
                    AS.getDict().replaceKey("/Resources", resources);
952
420
                }
953
                // Use mergeResources to force /Font to be local
954
7.29k
                QPDFObjectHandle res = resources;
955
7.29k
                res.mergeResources(Dictionary({{"/Font", Dictionary::empty()}}));
956
7.29k
                res.getKey("/Font").replaceKey(font_name, font);
957
7.29k
            }
958
8.41k
        }
959
960
8.62k
        if (Name Encoding = font["/Encoding"]) {
961
685
            if (Encoding == "/WinAnsiEncoding") {
962
301
                encoder = &QUtil::utf8_to_win_ansi;
963
384
            } else if (Encoding == "/MacRomanEncoding") {
964
44
                encoder = &QUtil::utf8_to_mac_roman;
965
44
            }
966
685
        }
967
8.62k
    }
968
969
12.4k
    V = (*encoder)(V, '?');
970
31.3k
    for (size_t i = 0; i < opt.size(); ++i) {
971
18.9k
        opt.at(i) = (*encoder)(opt.at(i), '?');
972
18.9k
    }
973
12.4k
    AS.addTokenFilter(
974
12.4k
        std::shared_ptr<QPDFObjectHandle::TokenFilter>(new ValueSetter(DA, V, opt, tf, bbox)));
975
12.4k
}