Coverage Report

Created: 2025-11-11 07:06

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