Coverage Report

Created: 2024-09-08 06:05

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