Coverage Report

Created: 2025-10-10 06:17

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