Coverage Report

Created: 2025-07-12 06:30

/src/qpdf/libqpdf/QPDFFormFieldObjectHelper.cc
Line
Count
Source (jump to first uncovered line)
1
#include <qpdf/QPDFFormFieldObjectHelper.hh>
2
3
#include <qpdf/Pl_QPDFTokenizer.hh>
4
#include <qpdf/QIntC.hh>
5
#include <qpdf/QPDFAcroFormDocumentHelper.hh>
6
#include <qpdf/QPDFAnnotationObjectHelper.hh>
7
#include <qpdf/QPDFObjectHandle_private.hh>
8
#include <qpdf/QTC.hh>
9
#include <qpdf/QUtil.hh>
10
#include <cstdlib>
11
12
QPDFFormFieldObjectHelper::QPDFFormFieldObjectHelper(QPDFObjectHandle oh) :
13
240k
    QPDFObjectHelper(oh),
14
240k
    m(new Members())
15
240k
{
16
240k
}
17
18
QPDFFormFieldObjectHelper::QPDFFormFieldObjectHelper() :
19
31.8k
    QPDFObjectHelper(QPDFObjectHandle::newNull()),
20
31.8k
    m(new Members())
21
31.8k
{
22
31.8k
}
23
24
bool
25
QPDFFormFieldObjectHelper::isNull()
26
0
{
27
0
    return oh().isNull();
28
0
}
29
30
QPDFFormFieldObjectHelper
31
QPDFFormFieldObjectHelper::getParent()
32
0
{
33
0
    return oh().getKey("/Parent"); // may be null
34
0
}
35
36
QPDFFormFieldObjectHelper
37
QPDFFormFieldObjectHelper::getTopLevelField(bool* is_different)
38
0
{
39
0
    auto top_field = oh();
40
0
    QPDFObjGen::set seen;
41
0
    while (seen.add(top_field) && !top_field.getKeyIfDict("/Parent").isNull()) {
42
0
        top_field = top_field.getKey("/Parent");
43
0
        if (is_different) {
44
0
            *is_different = true;
45
0
        }
46
0
    }
47
0
    return {top_field};
48
0
}
49
50
QPDFObjectHandle
51
QPDFFormFieldObjectHelper::getFieldFromAcroForm(std::string const& name)
52
152k
{
53
152k
    QPDFObjectHandle result = QPDFObjectHandle::newNull();
54
    // Fields are supposed to be indirect, so this should work.
55
152k
    QPDF* q = oh().getOwningQPDF();
56
152k
    if (!q) {
57
3.31k
        return result;
58
3.31k
    }
59
148k
    auto acroform = q->getRoot().getKey("/AcroForm");
60
148k
    if (!acroform.isDictionary()) {
61
0
        return result;
62
0
    }
63
148k
    return acroform.getKey(name);
64
148k
}
65
66
QPDFObjectHandle
67
QPDFFormFieldObjectHelper::getInheritableFieldValue(std::string const& name)
68
681k
{
69
681k
    QPDFObjectHandle node = oh();
70
681k
    if (!node.isDictionary()) {
71
2.25k
        return QPDFObjectHandle::newNull();
72
2.25k
    }
73
678k
    QPDFObjectHandle result(node.getKey(name));
74
678k
    if (result.isNull()) {
75
172k
        QPDFObjGen::set seen;
76
183k
        while (seen.add(node) && node.hasKey("/Parent")) {
77
29.7k
            node = node.getKey("/Parent");
78
29.7k
            result = node.getKey(name);
79
29.7k
            if (!result.isNull()) {
80
19.0k
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper non-trivial inheritance");
81
19.0k
                return result;
82
19.0k
            }
83
29.7k
        }
84
172k
    }
85
659k
    return result;
86
678k
}
87
88
std::string
89
QPDFFormFieldObjectHelper::getInheritableFieldValueAsString(std::string const& name)
90
79.9k
{
91
79.9k
    QPDFObjectHandle fv = getInheritableFieldValue(name);
92
79.9k
    std::string result;
93
79.9k
    if (fv.isString()) {
94
31.2k
        result = fv.getUTF8Value();
95
31.2k
    }
96
79.9k
    return result;
97
79.9k
}
98
99
std::string
100
QPDFFormFieldObjectHelper::getInheritableFieldValueAsName(std::string const& name)
101
359k
{
102
359k
    QPDFObjectHandle fv = getInheritableFieldValue(name);
103
359k
    std::string result;
104
359k
    if (fv.isName()) {
105
354k
        result = fv.getName();
106
354k
    }
107
359k
    return result;
108
359k
}
109
110
std::string
111
QPDFFormFieldObjectHelper::getFieldType()
112
359k
{
113
359k
    return getInheritableFieldValueAsName("/FT");
114
359k
}
115
116
std::string
117
QPDFFormFieldObjectHelper::getFullyQualifiedName()
118
21.6k
{
119
21.6k
    std::string result;
120
21.6k
    QPDFObjectHandle node = oh();
121
21.6k
    QPDFObjGen::set seen;
122
64.8k
    while (!node.isNull() && seen.add(node)) {
123
43.1k
        if (node.getKey("/T").isString()) {
124
40.8k
            if (!result.empty()) {
125
19.4k
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper non-trivial qualified name");
126
19.4k
                result = "." + result;
127
19.4k
            }
128
40.8k
            result = node.getKey("/T").getUTF8Value() + result;
129
40.8k
        }
130
43.1k
        node = node.getKey("/Parent");
131
43.1k
    }
132
21.6k
    return result;
133
21.6k
}
134
135
std::string
136
QPDFFormFieldObjectHelper::getPartialName()
137
0
{
138
0
    std::string result;
139
0
    if (oh().getKey("/T").isString()) {
140
0
        result = oh().getKey("/T").getUTF8Value();
141
0
    }
142
0
    return result;
143
0
}
144
145
std::string
146
QPDFFormFieldObjectHelper::getAlternativeName()
147
0
{
148
0
    if (oh().getKey("/TU").isString()) {
149
0
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper TU present");
150
0
        return oh().getKey("/TU").getUTF8Value();
151
0
    }
152
0
    QTC::TC("qpdf", "QPDFFormFieldObjectHelper TU absent");
153
0
    return getFullyQualifiedName();
154
0
}
155
156
std::string
157
QPDFFormFieldObjectHelper::getMappingName()
158
0
{
159
0
    if (oh().getKey("/TM").isString()) {
160
0
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper TM present");
161
0
        return oh().getKey("/TM").getUTF8Value();
162
0
    }
163
0
    QTC::TC("qpdf", "QPDFFormFieldObjectHelper TM absent");
164
0
    return getAlternativeName();
165
0
}
166
167
QPDFObjectHandle
168
QPDFFormFieldObjectHelper::getValue()
169
11.8k
{
170
11.8k
    return getInheritableFieldValue("/V");
171
11.8k
}
172
173
std::string
174
QPDFFormFieldObjectHelper::getValueAsString()
175
79.9k
{
176
79.9k
    return getInheritableFieldValueAsString("/V");
177
79.9k
}
178
179
QPDFObjectHandle
180
QPDFFormFieldObjectHelper::getDefaultValue()
181
0
{
182
0
    return getInheritableFieldValue("/DV");
183
0
}
184
185
std::string
186
QPDFFormFieldObjectHelper::getDefaultValueAsString()
187
0
{
188
0
    return getInheritableFieldValueAsString("/DV");
189
0
}
190
191
QPDFObjectHandle
192
QPDFFormFieldObjectHelper::getDefaultResources()
193
113k
{
194
113k
    return getFieldFromAcroForm("/DR");
195
113k
}
196
197
std::string
198
QPDFFormFieldObjectHelper::getDefaultAppearance()
199
79.9k
{
200
79.9k
    auto value = getInheritableFieldValue("/DA");
201
79.9k
    bool looked_in_acroform = false;
202
79.9k
    if (!value.isString()) {
203
38.2k
        value = getFieldFromAcroForm("/DA");
204
38.2k
        looked_in_acroform = true;
205
38.2k
    }
206
79.9k
    std::string result;
207
79.9k
    if (value.isString()) {
208
41.6k
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper DA present", looked_in_acroform ? 0 : 1);
209
41.6k
        result = value.getUTF8Value();
210
41.6k
    }
211
79.9k
    return result;
212
79.9k
}
213
214
int
215
QPDFFormFieldObjectHelper::getQuadding()
216
0
{
217
0
    QPDFObjectHandle fv = getInheritableFieldValue("/Q");
218
0
    bool looked_in_acroform = false;
219
0
    if (!fv.isInteger()) {
220
0
        fv = getFieldFromAcroForm("/Q");
221
0
        looked_in_acroform = true;
222
0
    }
223
0
    int result = 0;
224
0
    if (fv.isInteger()) {
225
0
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper Q present", looked_in_acroform ? 0 : 1);
226
0
        result = QIntC::to_int(fv.getIntValue());
227
0
    }
228
0
    return result;
229
0
}
230
231
int
232
QPDFFormFieldObjectHelper::getFlags()
233
106k
{
234
106k
    QPDFObjectHandle f = getInheritableFieldValue("/Ff");
235
106k
    return f.isInteger() ? f.getIntValueAsInt() : 0;
236
106k
}
237
238
bool
239
QPDFFormFieldObjectHelper::isText()
240
0
{
241
0
    return (getFieldType() == "/Tx");
242
0
}
243
244
bool
245
QPDFFormFieldObjectHelper::isCheckbox()
246
19.2k
{
247
19.2k
    return ((getFieldType() == "/Btn") && ((getFlags() & (ff_btn_radio | ff_btn_pushbutton)) == 0));
248
19.2k
}
249
250
bool
251
QPDFFormFieldObjectHelper::isChecked()
252
0
{
253
0
    return isCheckbox() && getValue().isName() && (getValue().getName() != "/Off");
254
0
}
255
256
bool
257
QPDFFormFieldObjectHelper::isRadioButton()
258
24.6k
{
259
24.6k
    return ((getFieldType() == "/Btn") && ((getFlags() & ff_btn_radio) == ff_btn_radio));
260
24.6k
}
261
262
bool
263
QPDFFormFieldObjectHelper::isPushbutton()
264
0
{
265
0
    return ((getFieldType() == "/Btn") && ((getFlags() & ff_btn_pushbutton) == ff_btn_pushbutton));
266
0
}
267
268
bool
269
QPDFFormFieldObjectHelper::isChoice()
270
123k
{
271
123k
    return (getFieldType() == "/Ch");
272
123k
}
273
274
std::vector<std::string>
275
QPDFFormFieldObjectHelper::getChoices()
276
43.4k
{
277
43.4k
    std::vector<std::string> result;
278
43.4k
    if (!isChoice()) {
279
0
        return result;
280
0
    }
281
245k
    for (auto const& item: getInheritableFieldValue("/Opt").as_array()) {
282
245k
        if (item.isString()) {
283
96.0k
            result.emplace_back(item.getUTF8Value());
284
149k
        } else if (item.isArray() && item.getArrayNItems() == 2) {
285
2.08k
            auto display = item.getArrayItem(1);
286
2.08k
            if (display.isString()) {
287
794
                result.emplace_back(display.getUTF8Value());
288
794
            }
289
2.08k
        }
290
245k
    }
291
43.4k
    return result;
292
43.4k
}
293
294
void
295
QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, QPDFObjectHandle value)
296
8.17k
{
297
8.17k
    oh().replaceKey(key, value);
298
8.17k
}
299
300
void
301
QPDFFormFieldObjectHelper::setFieldAttribute(std::string const& key, std::string const& utf8_value)
302
0
{
303
0
    oh().replaceKey(key, QPDFObjectHandle::newUnicodeString(utf8_value));
304
0
}
305
306
void
307
QPDFFormFieldObjectHelper::setV(QPDFObjectHandle value, bool need_appearances)
308
11.8k
{
309
11.8k
    if (getFieldType() == "/Btn") {
310
11.8k
        if (isCheckbox()) {
311
6.89k
            bool okay = false;
312
6.89k
            if (value.isName()) {
313
5.25k
                std::string name = value.getName();
314
5.25k
                okay = true;
315
                // Accept any value other than /Off to mean checked. Files have been seen that use
316
                // /1 or other values.
317
5.25k
                setCheckBoxValue((name != "/Off"));
318
5.25k
            }
319
6.89k
            if (!okay) {
320
1.64k
                oh().warnIfPossible(
321
1.64k
                    "ignoring attempt to set a checkbox field to a value whose type is not name");
322
1.64k
            }
323
6.89k
        } else if (isRadioButton()) {
324
4.95k
            if (value.isName()) {
325
4.35k
                setRadioButtonValue(value);
326
4.35k
            } else {
327
597
                oh().warnIfPossible(
328
597
                    "ignoring attempt to set a radio button field to an object that is not a name");
329
597
            }
330
4.95k
        } else if (isPushbutton()) {
331
0
            oh().warnIfPossible("ignoring attempt set the value of a pushbutton field");
332
0
        }
333
11.8k
        return;
334
11.8k
    }
335
0
    if (value.isString()) {
336
0
        setFieldAttribute("/V", QPDFObjectHandle::newUnicodeString(value.getUTF8Value()));
337
0
    } else {
338
0
        setFieldAttribute("/V", value);
339
0
    }
340
0
    if (need_appearances) {
341
0
        QPDF& qpdf = oh().getQPDF(
342
0
            "QPDFFormFieldObjectHelper::setV called with need_appearances = "
343
0
            "true on an object that is not associated with an owning QPDF");
344
0
        QPDFAcroFormDocumentHelper(qpdf).setNeedAppearances(true);
345
0
    }
346
0
}
347
348
void
349
QPDFFormFieldObjectHelper::setV(std::string const& utf8_value, bool need_appearances)
350
0
{
351
0
    setV(QPDFObjectHandle::newUnicodeString(utf8_value), need_appearances);
352
0
}
353
354
void
355
QPDFFormFieldObjectHelper::setRadioButtonValue(QPDFObjectHandle name)
356
6.65k
{
357
    // Set the value of a radio button field. This has the following specific behavior:
358
    // * If this is a radio button field that has a parent that is also a radio button field and has
359
    //   no explicit /V, call itself on the parent
360
    // * If this is a radio button field with children, set /V to the given value. Then, for each
361
    //   child, if the child has the specified value as one of its keys in the /N subdictionary of
362
    //   its /AP (i.e. its normal appearance stream dictionary), set /AS to name; otherwise, if /Off
363
    //   is a member, set /AS to /Off.
364
    // Note that we never turn on /NeedAppearances when setting a radio button field.
365
6.65k
    QPDFObjectHandle parent = oh().getKey("/Parent");
366
6.65k
    if (parent.isDictionary() && parent.getKey("/Parent").isNull()) {
367
3.05k
        QPDFFormFieldObjectHelper ph(parent);
368
3.05k
        if (ph.isRadioButton()) {
369
            // This is most likely one of the individual buttons. Try calling on the parent.
370
2.29k
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper set parent radio button");
371
2.29k
            ph.setRadioButtonValue(name);
372
2.29k
            return;
373
2.29k
        }
374
3.05k
    }
375
376
4.35k
    QPDFObjectHandle kids = oh().getKey("/Kids");
377
4.35k
    if (!(isRadioButton() && parent.isNull() && kids.isArray())) {
378
1.41k
        oh().warnIfPossible("don't know how to set the value of this field as a radio button");
379
1.41k
        return;
380
1.41k
    }
381
2.94k
    setFieldAttribute("/V", name);
382
8.60k
    for (auto const& kid: kids.as_array()) {
383
8.60k
        QPDFObjectHandle AP = kid.getKey("/AP");
384
8.60k
        QPDFObjectHandle annot;
385
8.60k
        if (AP.null()) {
386
            // The widget may be below. If there is more than one, just find the first one.
387
3.22k
            for (auto const& grandkid: kid.getKey("/Kids").as_array()) {
388
1.32k
                AP = grandkid.getKey("/AP");
389
1.32k
                if (!AP.null()) {
390
369
                    QTC::TC("qpdf", "QPDFFormFieldObjectHelper radio button grandkid");
391
369
                    annot = grandkid;
392
369
                    break;
393
369
                }
394
1.32k
            }
395
5.37k
        } else {
396
5.37k
            annot = kid;
397
5.37k
        }
398
8.60k
        if (!annot) {
399
2.83k
            QTC::TC("qpdf", "QPDFObjectHandle broken radio button");
400
2.83k
            oh().warnIfPossible("unable to set the value of this radio button");
401
2.83k
            continue;
402
2.83k
        }
403
5.77k
        if (AP.isDictionary() && AP.getKey("/N").isDictionary() &&
404
5.77k
            AP.getKey("/N").hasKey(name.getName())) {
405
624
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper turn on radio button");
406
624
            annot.replaceKey("/AS", name);
407
5.14k
        } else {
408
5.14k
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper turn off radio button");
409
5.14k
            annot.replaceKey("/AS", QPDFObjectHandle::newName("/Off"));
410
5.14k
        }
411
5.77k
    }
412
2.94k
}
413
414
void
415
QPDFFormFieldObjectHelper::setCheckBoxValue(bool value)
416
5.25k
{
417
5.25k
    QPDFObjectHandle AP = oh().getKey("/AP");
418
5.25k
    QPDFObjectHandle annot;
419
5.25k
    if (AP.null()) {
420
        // The widget may be below. If there is more than one, just
421
        // find the first one.
422
2.14k
        QPDFObjectHandle kids = oh().getKey("/Kids");
423
2.45k
        for (auto const& kid: oh().getKey("/Kids").as_array(qpdf::strict)) {
424
2.45k
            AP = kid.getKey("/AP");
425
2.45k
            if (!AP.null()) {
426
886
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper checkbox kid widget");
427
886
                annot = kid;
428
886
                break;
429
886
            }
430
2.45k
        }
431
3.11k
    } else {
432
3.11k
        annot = oh();
433
3.11k
    }
434
5.25k
    std::string on_value;
435
5.25k
    if (value) {
436
        // Set the "on" value to the first value in the appearance stream's normal state dictionary
437
        // that isn't /Off. If not found, fall back to /Yes.
438
4.08k
        if (AP.isDictionary()) {
439
3.19k
            for (auto const& item: AP.getKey("/N").as_dictionary()) {
440
2.90k
                if (item.first != "/Off") {
441
1.60k
                    on_value = item.first;
442
1.60k
                    break;
443
1.60k
                }
444
2.90k
            }
445
3.19k
        }
446
4.08k
        if (on_value.empty()) {
447
2.48k
            on_value = "/Yes";
448
2.48k
        }
449
4.08k
    }
450
451
    // Set /AS to the on value or /Off in addition to setting /V.
452
5.25k
    QPDFObjectHandle name = QPDFObjectHandle::newName(value ? on_value : "/Off");
453
5.25k
    setFieldAttribute("/V", name);
454
5.25k
    if (!annot) {
455
1.23k
        QTC::TC("qpdf", "QPDFObjectHandle broken checkbox");
456
1.23k
        oh().warnIfPossible("unable to set the value of this checkbox");
457
1.23k
        return;
458
1.23k
    }
459
4.02k
    QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS");
460
4.02k
    annot.replaceKey("/AS", name);
461
4.02k
}
462
463
void
464
QPDFFormFieldObjectHelper::generateAppearance(QPDFAnnotationObjectHelper& aoh)
465
84.1k
{
466
84.1k
    std::string ft = getFieldType();
467
    // Ignore field types we don't know how to generate appearances for. Button fields don't really
468
    // need them -- see code in QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded.
469
84.1k
    if ((ft == "/Tx") || (ft == "/Ch")) {
470
80.5k
        generateTextAppearance(aoh);
471
80.5k
    }
472
84.1k
}
473
474
namespace
475
{
476
    class ValueSetter: public QPDFObjectHandle::TokenFilter
477
    {
478
      public:
479
        ValueSetter(
480
            std::string const& DA,
481
            std::string const& V,
482
            std::vector<std::string> const& opt,
483
            double tf,
484
            QPDFObjectHandle::Rectangle const& bbox);
485
79.9k
        ~ValueSetter() override = default;
486
        void handleToken(QPDFTokenizer::Token const&) override;
487
        void handleEOF() override;
488
        void writeAppearance();
489
490
      private:
491
        std::string DA;
492
        std::string V;
493
        std::vector<std::string> opt;
494
        double tf;
495
        QPDFObjectHandle::Rectangle bbox;
496
        enum { st_top, st_bmc, st_emc, st_end } state{st_top};
497
        bool replaced{false};
498
    };
499
} // namespace
500
501
ValueSetter::ValueSetter(
502
    std::string const& DA,
503
    std::string const& V,
504
    std::vector<std::string> const& opt,
505
    double tf,
506
    QPDFObjectHandle::Rectangle const& bbox) :
507
79.9k
    DA(DA),
508
79.9k
    V(V),
509
79.9k
    opt(opt),
510
79.9k
    tf(tf),
511
79.9k
    bbox(bbox)
512
79.9k
{
513
79.9k
}
514
515
void
516
ValueSetter::handleToken(QPDFTokenizer::Token const& token)
517
13.5M
{
518
13.5M
    QPDFTokenizer::token_type_e ttype = token.getType();
519
13.5M
    std::string value = token.getValue();
520
13.5M
    bool do_replace = false;
521
13.5M
    switch (state) {
522
1.12M
    case st_top:
523
1.12M
        writeToken(token);
524
1.12M
        if (token.isWord("BMC")) {
525
26.6k
            state = st_bmc;
526
26.6k
        }
527
1.12M
        break;
528
529
53.5k
    case st_bmc:
530
53.5k
        if ((ttype == QPDFTokenizer::tt_space) || (ttype == QPDFTokenizer::tt_comment)) {
531
26.9k
            writeToken(token);
532
26.9k
        } else {
533
26.6k
            state = st_emc;
534
26.6k
        }
535
        // fall through to emc
536
537
1.20M
    case st_emc:
538
1.20M
        if (token.isWord("EMC")) {
539
26.3k
            do_replace = true;
540
26.3k
            state = st_end;
541
26.3k
        }
542
1.20M
        break;
543
544
11.1M
    case st_end:
545
11.1M
        writeToken(token);
546
11.1M
        break;
547
13.5M
    }
548
13.5M
    if (do_replace) {
549
26.3k
        writeAppearance();
550
26.3k
    }
551
13.5M
}
552
553
void
554
ValueSetter::handleEOF()
555
27.8k
{
556
27.8k
    if (!replaced) {
557
1.34k
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper replaced BMC at EOF");
558
1.34k
        write("/Tx BMC\n");
559
1.34k
        writeAppearance();
560
1.34k
    }
561
27.8k
}
562
563
void
564
ValueSetter::writeAppearance()
565
27.6k
{
566
27.6k
    replaced = true;
567
568
    // This code does not take quadding into consideration because doing so requires font metric
569
    // information, which we don't have in many cases.
570
571
27.6k
    double tfh = 1.2 * tf;
572
27.6k
    int dx = 1;
573
574
    // Write one or more lines, centered vertically, possibly with one row highlighted.
575
576
27.6k
    auto max_rows = static_cast<size_t>((bbox.ury - bbox.lly) / tfh);
577
27.6k
    bool highlight = false;
578
27.6k
    size_t highlight_idx = 0;
579
580
27.6k
    std::vector<std::string> lines;
581
27.6k
    if (opt.empty() || (max_rows < 2)) {
582
22.3k
        lines.push_back(V);
583
22.3k
    } else {
584
        // Figure out what rows to write
585
5.30k
        size_t nopt = opt.size();
586
5.30k
        size_t found_idx = 0;
587
5.30k
        bool found = false;
588
37.2k
        for (found_idx = 0; found_idx < nopt; ++found_idx) {
589
33.1k
            if (opt.at(found_idx) == V) {
590
1.27k
                found = true;
591
1.27k
                break;
592
1.27k
            }
593
33.1k
        }
594
5.30k
        if (found) {
595
            // Try to make the found item the second one, but adjust for under/overflow.
596
1.27k
            int wanted_first = QIntC::to_int(found_idx) - 1;
597
1.27k
            int wanted_last = QIntC::to_int(found_idx + max_rows) - 2;
598
1.27k
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper list found");
599
1.27k
            if (wanted_first < 0) {
600
269
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper list first too low");
601
269
                wanted_last -= wanted_first;
602
269
                wanted_first = 0;
603
269
            }
604
1.27k
            if (wanted_last >= QIntC::to_int(nopt)) {
605
770
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper list last too high");
606
770
                auto diff = wanted_last - QIntC::to_int(nopt) + 1;
607
770
                wanted_first = std::max(0, wanted_first - diff);
608
770
                wanted_last -= diff;
609
770
            }
610
1.27k
            highlight = true;
611
1.27k
            highlight_idx = found_idx - QIntC::to_size(wanted_first);
612
21.8k
            for (size_t i = QIntC::to_size(wanted_first); i <= QIntC::to_size(wanted_last); ++i) {
613
20.5k
                lines.push_back(opt.at(i));
614
20.5k
            }
615
4.03k
        } else {
616
4.03k
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper list not found");
617
            // include our value and the first n-1 rows
618
4.03k
            highlight_idx = 0;
619
4.03k
            highlight = true;
620
4.03k
            lines.push_back(V);
621
25.7k
            for (size_t i = 0; ((i < nopt) && (i < (max_rows - 1))); ++i) {
622
21.7k
                lines.push_back(opt.at(i));
623
21.7k
            }
624
4.03k
        }
625
5.30k
    }
626
627
    // Write the lines centered vertically, highlighting if needed
628
27.6k
    size_t nlines = lines.size();
629
27.6k
    double dy = bbox.ury - ((bbox.ury - bbox.lly - (static_cast<double>(nlines) * tfh)) / 2.0);
630
27.6k
    if (highlight) {
631
5.27k
        write(
632
5.27k
            "q\n0.85 0.85 0.85 rg\n" + QUtil::double_to_string(bbox.llx) + " " +
633
5.27k
            QUtil::double_to_string(
634
5.27k
                bbox.lly + dy - (tfh * (static_cast<double>(highlight_idx + 1)))) +
635
5.27k
            " " + QUtil::double_to_string(bbox.urx - bbox.llx) + " " +
636
5.27k
            QUtil::double_to_string(tfh) + " re f\nQ\n");
637
5.27k
    }
638
27.6k
    dy -= tf;
639
27.6k
    write("q\nBT\n" + DA + "\n");
640
96.4k
    for (size_t i = 0; i < nlines; ++i) {
641
        // We could adjust Tm to translate to the beginning the first line, set TL to tfh, and use
642
        // T* for each subsequent line, but doing this would require extracting any Tm from DA,
643
        // which doesn't seem really worth the effort.
644
68.7k
        if (i == 0) {
645
27.6k
            write(
646
27.6k
                QUtil::double_to_string(bbox.llx + static_cast<double>(dx)) + " " +
647
27.6k
                QUtil::double_to_string(bbox.lly + static_cast<double>(dy)) + " Td\n");
648
41.0k
        } else {
649
41.0k
            write("0 " + QUtil::double_to_string(-tfh) + " Td\n");
650
41.0k
        }
651
68.7k
        write(QPDFObjectHandle::newString(lines.at(i)).unparse() + " Tj\n");
652
68.7k
    }
653
27.6k
    write("ET\nQ\nEMC");
654
27.6k
}
655
656
namespace
657
{
658
    class TfFinder final: public QPDFObjectHandle::TokenFilter
659
    {
660
      public:
661
79.9k
        TfFinder() = default;
662
79.9k
        ~TfFinder() final = default;
663
664
        void
665
        handleToken(QPDFTokenizer::Token const& token) final
666
7.87M
        {
667
7.87M
            auto ttype = token.getType();
668
7.87M
            auto const& value = token.getValue();
669
7.87M
            DA.emplace_back(token.getRawValue());
670
7.87M
            switch (ttype) {
671
558k
            case QPDFTokenizer::tt_integer:
672
730k
            case QPDFTokenizer::tt_real:
673
730k
                last_num = strtod(value.c_str(), nullptr);
674
730k
                last_num_idx = QIntC::to_int(DA.size() - 1);
675
730k
                break;
676
677
1.22M
            case QPDFTokenizer::tt_name:
678
1.22M
                last_name = value;
679
1.22M
                break;
680
681
583k
            case QPDFTokenizer::tt_word:
682
583k
                if (token.isWord("Tf")) {
683
38.8k
                    if ((last_num > 1.0) && (last_num < 1000.0)) {
684
                        // These ranges are arbitrary but keep us from doing insane things or
685
                        // suffering from over/underflow
686
30.2k
                        tf = last_num;
687
30.2k
                    }
688
38.8k
                    tf_idx = last_num_idx;
689
38.8k
                    font_name = last_name;
690
38.8k
                }
691
583k
                break;
692
693
5.33M
            default:
694
5.33M
                break;
695
7.87M
            }
696
7.87M
        }
697
698
        double
699
        getTf() const
700
79.9k
        {
701
79.9k
            return tf;
702
79.9k
        }
703
        std::string
704
        getFontName() const
705
79.9k
        {
706
79.9k
            return font_name;
707
79.9k
        }
708
709
        std::string
710
        getDA()
711
79.9k
        {
712
79.9k
            std::string result;
713
79.9k
            int i = -1;
714
7.87M
            for (auto const& cur: DA) {
715
7.87M
                if (++i == tf_idx) {
716
34.5k
                    double delta = strtod(cur.c_str(), nullptr) - tf;
717
34.5k
                    if (delta > 0.001 || delta < -0.001) {
718
                        // tf doesn't match the font size passed to Tf, so substitute.
719
7.75k
                        QTC::TC("qpdf", "QPDFFormFieldObjectHelper fallback Tf");
720
7.75k
                        result += QUtil::double_to_string(tf);
721
7.75k
                        continue;
722
7.75k
                    }
723
34.5k
                }
724
7.86M
                result += cur;
725
7.86M
            }
726
79.9k
            return result;
727
79.9k
        }
728
729
      private:
730
        double tf{11.0};
731
        int tf_idx{-1};
732
        std::string font_name;
733
        double last_num{0.0};
734
        int last_num_idx{-1};
735
        std::string last_name;
736
        std::vector<std::string> DA;
737
    };
738
} // namespace
739
740
QPDFObjectHandle
741
QPDFFormFieldObjectHelper::getFontFromResource(QPDFObjectHandle resources, std::string const& name)
742
64.7k
{
743
64.7k
    QPDFObjectHandle result;
744
64.7k
    if (resources.isDictionary() && resources.getKey("/Font").isDictionary() &&
745
64.7k
        resources.getKey("/Font").hasKey(name)) {
746
2.96k
        result = resources.getKey("/Font").getKey(name);
747
2.96k
    }
748
64.7k
    return result;
749
64.7k
}
750
751
void
752
QPDFFormFieldObjectHelper::generateTextAppearance(QPDFAnnotationObjectHelper& aoh)
753
80.5k
{
754
80.5k
    QPDFObjectHandle AS = aoh.getAppearanceStream("/N");
755
80.5k
    if (AS.isNull()) {
756
5.93k
        QTC::TC("qpdf", "QPDFFormFieldObjectHelper create AS from scratch");
757
5.93k
        QPDFObjectHandle::Rectangle rect = aoh.getRect();
758
5.93k
        QPDFObjectHandle::Rectangle bbox(0, 0, rect.urx - rect.llx, rect.ury - rect.lly);
759
5.93k
        QPDFObjectHandle dict = QPDFObjectHandle::parse(
760
5.93k
            "<< /Resources << /ProcSet [ /PDF /Text ] >> /Type /XObject /Subtype /Form >>");
761
5.93k
        dict.replaceKey("/BBox", QPDFObjectHandle::newFromRectangle(bbox));
762
5.93k
        AS = QPDFObjectHandle::newStream(oh().getOwningQPDF(), "/Tx BMC\nEMC\n");
763
5.93k
        AS.replaceDict(dict);
764
5.93k
        QPDFObjectHandle AP = aoh.getAppearanceDictionary();
765
5.93k
        if (AP.isNull()) {
766
1.47k
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper create AP from scratch");
767
1.47k
            aoh.getObjectHandle().replaceKey("/AP", QPDFObjectHandle::newDictionary());
768
1.47k
            AP = aoh.getAppearanceDictionary();
769
1.47k
        }
770
5.93k
        AP.replaceKey("/N", AS);
771
5.93k
    }
772
80.5k
    if (!AS.isStream()) {
773
0
        aoh.getObjectHandle().warnIfPossible("unable to get normal appearance stream for update");
774
0
        return;
775
0
    }
776
80.5k
    QPDFObjectHandle bbox_obj = AS.getDict().getKey("/BBox");
777
80.5k
    if (!bbox_obj.isRectangle()) {
778
633
        aoh.getObjectHandle().warnIfPossible("unable to get appearance stream bounding box");
779
633
        return;
780
633
    }
781
79.9k
    QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle();
782
79.9k
    std::string DA = getDefaultAppearance();
783
79.9k
    std::string V = getValueAsString();
784
79.9k
    std::vector<std::string> opt;
785
79.9k
    if (isChoice() && ((getFlags() & ff_ch_combo) == 0)) {
786
43.4k
        opt = getChoices();
787
43.4k
    }
788
789
79.9k
    TfFinder tff;
790
79.9k
    Pl_QPDFTokenizer tok("tf", &tff);
791
79.9k
    tok.writeString(DA);
792
79.9k
    tok.finish();
793
79.9k
    double tf = tff.getTf();
794
79.9k
    DA = tff.getDA();
795
796
79.9k
    std::string (*encoder)(std::string const&, char) = &QUtil::utf8_to_ascii;
797
79.9k
    std::string font_name = tff.getFontName();
798
79.9k
    if (!font_name.empty()) {
799
        // See if the font is encoded with something we know about.
800
33.3k
        QPDFObjectHandle resources = AS.getDict().getKey("/Resources");
801
33.3k
        QPDFObjectHandle font = getFontFromResource(resources, font_name);
802
33.3k
        bool found_font_in_dr = false;
803
33.3k
        if (!font) {
804
31.3k
            QPDFObjectHandle dr = getDefaultResources();
805
31.3k
            font = getFontFromResource(dr, font_name);
806
31.3k
            found_font_in_dr = font.isDictionary();
807
31.3k
        }
808
33.3k
        if (found_font_in_dr && resources.isDictionary()) {
809
474
            QTC::TC("qpdf", "QPDFFormFieldObjectHelper get font from /DR");
810
474
            if (resources.isIndirect()) {
811
14
                resources = resources.getQPDF().makeIndirectObject(resources.shallowCopy());
812
14
                AS.getDict().replaceKey("/Resources", resources);
813
14
            }
814
            // Use mergeResources to force /Font to be local
815
474
            resources.mergeResources("<< /Font << >> >>"_qpdf);
816
474
            resources.getKey("/Font").replaceKey(font_name, font);
817
474
        }
818
819
33.3k
        if (font.isDictionary() && font.getKey("/Encoding").isName()) {
820
2.15k
            std::string encoding = font.getKey("/Encoding").getName();
821
2.15k
            if (encoding == "/WinAnsiEncoding") {
822
777
                QTC::TC("qpdf", "QPDFFormFieldObjectHelper WinAnsi");
823
777
                encoder = &QUtil::utf8_to_win_ansi;
824
1.37k
            } else if (encoding == "/MacRomanEncoding") {
825
223
                encoder = &QUtil::utf8_to_mac_roman;
826
223
            }
827
2.15k
        }
828
33.3k
    }
829
830
79.9k
    V = (*encoder)(V, '?');
831
176k
    for (size_t i = 0; i < opt.size(); ++i) {
832
96.8k
        opt.at(i) = (*encoder)(opt.at(i), '?');
833
96.8k
    }
834
835
79.9k
    AS.addTokenFilter(
836
79.9k
        std::shared_ptr<QPDFObjectHandle::TokenFilter>(new ValueSetter(DA, V, opt, tf, bbox)));
837
79.9k
}