diff options
Diffstat (limited to 'libqpdf/QPDFFormFieldObjectHelper.cc')
-rw-r--r-- | libqpdf/QPDFFormFieldObjectHelper.cc | 686 |
1 files changed, 685 insertions, 1 deletions
diff --git a/libqpdf/QPDFFormFieldObjectHelper.cc b/libqpdf/QPDFFormFieldObjectHelper.cc index 283b632d..38755388 100644 --- a/libqpdf/QPDFFormFieldObjectHelper.cc +++ b/libqpdf/QPDFFormFieldObjectHelper.cc @@ -1,6 +1,10 @@ #include <qpdf/QPDFFormFieldObjectHelper.hh> #include <qpdf/QTC.hh> #include <qpdf/QPDFAcroFormDocumentHelper.hh> +#include <qpdf/QPDFAnnotationObjectHelper.hh> +#include <qpdf/QUtil.hh> +#include <qpdf/Pl_QPDFTokenizer.hh> +#include <stdlib.h> QPDFFormFieldObjectHelper::Members::~Members() { @@ -190,6 +194,70 @@ QPDFFormFieldObjectHelper::getQuadding() return result; } +int +QPDFFormFieldObjectHelper::getFlags() +{ + QPDFObjectHandle f = getInheritableFieldValue("/Ff"); + return f.isInteger() ? f.getIntValue() : 0; +} + +bool +QPDFFormFieldObjectHelper::isText() +{ + return (getFieldType() == "/Tx"); +} + +bool +QPDFFormFieldObjectHelper::isCheckbox() +{ + return ((getFieldType() == "/Btn") && + ((getFlags() & (ff_btn_radio | ff_btn_pushbutton)) == 0)); +} + +bool +QPDFFormFieldObjectHelper::isRadioButton() +{ + return ((getFieldType() == "/Btn") && + ((getFlags() & ff_btn_radio) == ff_btn_radio)); +} + +bool +QPDFFormFieldObjectHelper::isPushbutton() +{ + return ((getFieldType() == "/Btn") && + ((getFlags() & ff_btn_pushbutton) == ff_btn_pushbutton)); +} + +bool +QPDFFormFieldObjectHelper::isChoice() +{ + return (getFieldType() == "/Ch"); +} + +std::vector<std::string> +QPDFFormFieldObjectHelper::getChoices() +{ + std::vector<std::string> result; + if (! isChoice()) + { + return result; + } + QPDFObjectHandle opt = getInheritableFieldValue("/Opt"); + if (opt.isArray()) + { + size_t n = opt.getArrayNItems(); + for (size_t i = 0; i < n; ++i) + { + QPDFObjectHandle item = opt.getArrayItem(i); + if (item.isString()) + { + result.push_back(item.getUTF8Value()); + } + } + } + return result; +} + void QPDFFormFieldObjectHelper::setFieldAttribute( std::string const& key, QPDFObjectHandle value) @@ -208,7 +276,56 @@ void QPDFFormFieldObjectHelper::setV( QPDFObjectHandle value, bool need_appearances) { - setFieldAttribute("/V", value); + if (getFieldType() == "/Btn") + { + if (isCheckbox()) + { + bool okay = false; + if (value.isName()) + { + std::string name = value.getName(); + if ((name == "/Yes") || (name == "/Off")) + { + okay = true; + setCheckBoxValue((name == "/Yes")); + } + } + if (! okay) + { + this->oh.warnIfPossible( + "ignoring attempt to set a checkbox field to a" + " value of other than /Yes or /Off"); + } + } + else if (isRadioButton()) + { + if (value.isName()) + { + setRadioButtonValue(value); + } + else + { + this->oh.warnIfPossible( + "ignoring attempt to set a radio button field to" + " an object that is not a name"); + } + } + else if (isPushbutton()) + { + this->oh.warnIfPossible( + "ignoring attempt set the value of a pushbutton field"); + } + return; + } + if (value.isString()) + { + setFieldAttribute( + "/V", QPDFObjectHandle::newUnicodeString(value.getUTF8Value())); + } + else + { + setFieldAttribute("/V", value); + } if (need_appearances) { QPDF* qpdf = this->oh.getOwningQPDF(); @@ -230,3 +347,570 @@ QPDFFormFieldObjectHelper::setV( setV(QPDFObjectHandle::newUnicodeString(utf8_value), need_appearances); } + +void +QPDFFormFieldObjectHelper::setRadioButtonValue(QPDFObjectHandle name) +{ + // Set the value of a radio button field. This has the following + // specific behavior: + // * If this is a radio button field that has a parent that is + // also a radio button field and has no explicit /V, call itself + // on the parent + // * If this is a radio button field with children, set /V to the + // given value. Then, for each child, if the child has the + // specified value as one of its keys in the /N subdictionary of + // its /AP (i.e. its normal appearance stream dictionary), set + // /AS to name; otherwise, if /Off is a member, set /AS to /Off. + // Note that we never turn on /NeedAppearances when setting a + // radio button field. + QPDFObjectHandle parent = this->oh.getKey("/Parent"); + if (parent.isDictionary() && parent.getKey("/Parent").isNull()) + { + QPDFFormFieldObjectHelper ph(parent); + if (ph.isRadioButton()) + { + // This is most likely one of the individual buttons. Try + // calling on the parent. + QTC::TC("qpdf", "QPDFFormFieldObjectHelper set parent radio button"); + ph.setRadioButtonValue(name); + return; + } + } + + QPDFObjectHandle kids = this->oh.getKey("/Kids"); + if (! (isRadioButton() && parent.isNull() && kids.isArray())) + { + this->oh.warnIfPossible("don't know how to set the value" + " of this field as a radio button"); + return; + } + setFieldAttribute("/V", name); + int nkids = kids.getArrayNItems(); + for (int i = 0; i < nkids; ++i) + { + QPDFObjectHandle kid = kids.getArrayItem(i); + QPDFObjectHandle AP = kid.getKey("/AP"); + QPDFObjectHandle annot; + if (AP.isNull()) + { + // The widget may be below. If there is more than one, + // just find the first one. + QPDFObjectHandle grandkids = kid.getKey("/Kids"); + if (grandkids.isArray()) + { + int ngrandkids = grandkids.getArrayNItems(); + for (int j = 0; j < ngrandkids; ++j) + { + QPDFObjectHandle grandkid = grandkids.getArrayItem(j); + AP = grandkid.getKey("/AP"); + if (! AP.isNull()) + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper radio button grandkid widget"); + annot = grandkid; + break; + } + } + } + } + else + { + annot = kid; + } + if (! annot.isInitialized()) + { + QTC::TC("qpdf", "QPDFObjectHandle broken radio button"); + this->oh.warnIfPossible( + "unable to set the value of this radio button"); + continue; + } + if (AP.isDictionary() && + AP.getKey("/N").isDictionary() && + AP.getKey("/N").hasKey(name.getName())) + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper turn on radio button"); + annot.replaceKey("/AS", name); + } + else + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper turn off radio button"); + annot.replaceKey("/AS", QPDFObjectHandle::newName("/Off")); + } + } +} + +void +QPDFFormFieldObjectHelper::setCheckBoxValue(bool value) +{ + // Set /AS to /Yes or /Off in addition to setting /V. + QPDFObjectHandle name = + QPDFObjectHandle::newName(value ? "/Yes" : "/Off"); + setFieldAttribute("/V", name); + QPDFObjectHandle AP = this->oh.getKey("/AP"); + QPDFObjectHandle annot; + if (AP.isNull()) + { + // The widget may be below. If there is more than one, just + // find the first one. + QPDFObjectHandle kids = this->oh.getKey("/Kids"); + if (kids.isArray()) + { + int nkids = kids.getArrayNItems(); + for (int i = 0; i < nkids; ++i) + { + QPDFObjectHandle kid = kids.getArrayItem(i); + AP = kid.getKey("/AP"); + if (! AP.isNull()) + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper checkbox kid widget"); + annot = kid; + break; + } + } + } + } + else + { + annot = this->oh; + } + if (! annot.isInitialized()) + { + QTC::TC("qpdf", "QPDFObjectHandle broken checkbox"); + this->oh.warnIfPossible( + "unable to set the value of this checkbox"); + return; + } + QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS"); + annot.replaceKey("/AS", name); +} + +void +QPDFFormFieldObjectHelper::generateAppearance(QPDFAnnotationObjectHelper& aoh) +{ + std::string ft = getFieldType(); + // Ignore field types we don't know how to generate appearances + // for. Button fields don't really need them -- see code in + // QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded. + if ((ft == "/Tx") || (ft == "/Ch")) + { + generateTextAppearance(aoh); + } +} + +class ValueSetter: public QPDFObjectHandle::TokenFilter +{ + public: + ValueSetter(std::string const& DA, std::string const& V, + std::vector<std::string> const& opt, double tf, + QPDFObjectHandle::Rectangle const& bbox); + virtual ~ValueSetter() + { + } + virtual void handleToken(QPDFTokenizer::Token const&); + virtual void handleEOF(); + void writeAppearance(); + + private: + std::string DA; + std::string V; + std::vector<std::string> opt; + double tf; + QPDFObjectHandle::Rectangle bbox; + enum { st_top, st_bmc, st_emc, st_end } state; + bool replaced; +}; + +ValueSetter::ValueSetter(std::string const& DA, std::string const& V, + std::vector<std::string> const& opt, double tf, + QPDFObjectHandle::Rectangle const& bbox) : + DA(DA), + V(V), + opt(opt), + tf(tf), + bbox(bbox), + state(st_top), + replaced(false) +{ +} + +void +ValueSetter::handleToken(QPDFTokenizer::Token const& token) +{ + QPDFTokenizer::token_type_e ttype = token.getType(); + std::string value = token.getValue(); + bool do_replace = false; + switch (state) + { + case st_top: + writeToken(token); + if ((ttype == QPDFTokenizer::tt_word) && (value == "BMC")) + { + state = st_bmc; + } + break; + + case st_bmc: + if ((ttype == QPDFTokenizer::tt_space) || + (ttype == QPDFTokenizer::tt_comment)) + { + writeToken(token); + } + else + { + state = st_emc; + } + // fall through to emc + + case st_emc: + if ((ttype == QPDFTokenizer::tt_word) && (value == "EMC")) + { + do_replace = true; + state = st_end; + } + break; + + case st_end: + writeToken(token); + break; + } + if (do_replace) + { + writeAppearance(); + } +} + +void +ValueSetter::handleEOF() +{ + if (! this->replaced) + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper replaced BMC at EOF"); + write("/Tx BMC\n"); + writeAppearance(); + } +} + +void +ValueSetter::writeAppearance() +{ + this->replaced = true; + + // This code does not take quadding into consideration because + // doing so requires font metric information, which we don't + // have in many cases. + + double tfh = 1.2 * tf; + int dx = 1; + + // Write one or more lines, centered vertically, possibly with + // one row highlighted. + + size_t max_rows = static_cast<size_t>((bbox.ury - bbox.lly) / tfh); + bool highlight = false; + size_t highlight_idx = 0; + + std::vector<std::string> lines; + if (opt.empty() || (max_rows < 2)) + { + lines.push_back(V); + } + else + { + // Figure out what rows to write + size_t nopt = opt.size(); + size_t found_idx = 0; + bool found = false; + for (found_idx = 0; found_idx < nopt; ++found_idx) + { + if (opt.at(found_idx) == V) + { + found = true; + break; + } + } + if (found) + { + // Try to make the found item the second one, but + // adjust for under/overflow. + int wanted_first = found_idx - 1; + int wanted_last = found_idx + max_rows - 2; + QTC::TC("qpdf", "QPDFFormFieldObjectHelper list found"); + while (wanted_first < 0) + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper list first too low"); + ++wanted_first; + ++wanted_last; + } + while (wanted_last >= static_cast<int>(nopt)) + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper list last too high"); + if (wanted_first > 0) + { + --wanted_first; + } + --wanted_last; + } + highlight = true; + highlight_idx = found_idx - wanted_first; + for (int i = wanted_first; i <= wanted_last; ++i) + { + lines.push_back(opt.at(i)); + } + } + else + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper list not found"); + // include our value and the first n-1 rows + highlight_idx = 0; + highlight = true; + lines.push_back(V); + for (size_t i = 0; ((i < nopt) && (i < (max_rows - 1))); ++i) + { + lines.push_back(opt.at(i)); + } + } + } + + // Write the lines centered vertically, highlighting if needed + size_t nlines = lines.size(); + double dy = bbox.ury - ((bbox.ury - bbox.lly - (nlines * tfh)) / 2.0); + if (highlight) + { + write("q\n0.85 0.85 0.85 rg\n" + + QUtil::int_to_string(bbox.llx) + " " + + QUtil::double_to_string(bbox.lly + dy - + (tfh * (highlight_idx + 1))) + " " + + QUtil::int_to_string(bbox.urx - bbox.llx) + " " + + QUtil::double_to_string(tfh) + + " re f\nQ\n"); + } + dy -= tf; + write("q\nBT\n" + DA + "\n"); + for (size_t i = 0; i < nlines; ++i) + { + // We could adjust Tm to translate to the beginning the first + // line, set TL to tfh, and use T* for each subsequent line, + // but doing this would require extracting any Tm from DA, + // which doesn't seem really worth the effort. + if (i == 0) + { + write(QUtil::int_to_string(bbox.llx + dx) + " " + + QUtil::double_to_string(bbox.lly + dy) + " Td\n"); + } + else + { + write("0 " + QUtil::double_to_string(-tfh) + " Td\n"); + } + write(QPDFObjectHandle::newString(lines.at(i)).unparse() + " Tj\n"); + } + write("ET\nQ\nEMC"); +} + +class TfFinder: public QPDFObjectHandle::TokenFilter +{ + public: + TfFinder(); + virtual ~TfFinder() + { + } + virtual void handleToken(QPDFTokenizer::Token const&); + double getTf(); + std::string getFontName(); + std::string getDA(); + + private: + double tf; + size_t tf_idx; + std::string font_name; + double last_num; + size_t last_num_idx; + std::string last_name; + std::vector<std::string> DA; +}; + +TfFinder::TfFinder() : + tf(11.0), + tf_idx(0), + last_num(0.0), + last_num_idx(0) +{ +} + +void +TfFinder::handleToken(QPDFTokenizer::Token const& token) +{ + QPDFTokenizer::token_type_e ttype = token.getType(); + std::string value = token.getValue(); + DA.push_back(token.getRawValue()); + switch (ttype) + { + case QPDFTokenizer::tt_integer: + case QPDFTokenizer::tt_real: + last_num = strtod(value.c_str(), 0); + last_num_idx = DA.size() - 1; + break; + + case QPDFTokenizer::tt_name: + last_name = value; + break; + + case QPDFTokenizer::tt_word: + if ((value == "Tf") && + (last_num > 1.0) && + (last_num < 1000.0)) + { + // These ranges are arbitrary but keep us from doing + // insane things or suffering from over/underflow + tf = last_num; + } + tf_idx = last_num_idx; + font_name = last_name; + break; + + default: + break; + } +} + +double +TfFinder::getTf() +{ + return this->tf; +} + +std::string +TfFinder::getDA() +{ + std::string result; + size_t n = this->DA.size(); + for (size_t i = 0; i < n; ++i) + { + std::string cur = this->DA.at(i); + if (i == tf_idx) + { + double delta = strtod(cur.c_str(), 0) - this->tf; + if ((delta > 0.001) || (delta < -0.001)) + { + // tf doesn't match the font size passed to Tf, so + // substitute. + QTC::TC("qpdf", "QPDFFormFieldObjectHelper fallback Tf"); + cur = QUtil::double_to_string(tf); + } + } + result += cur; + } + return result; +} + +std::string +TfFinder::getFontName() +{ + return this->font_name; +} + +QPDFObjectHandle +QPDFFormFieldObjectHelper::getFontFromResource( + QPDFObjectHandle resources, std::string const& name) +{ + QPDFObjectHandle result; + if (resources.isDictionary() && + resources.getKey("/Font").isDictionary() && + resources.getKey("/Font").hasKey(name)) + { + result = resources.getKey("/Font").getKey(name); + } + return result; +} + +void +QPDFFormFieldObjectHelper::generateTextAppearance( + QPDFAnnotationObjectHelper& aoh) +{ + QPDFObjectHandle AS = aoh.getAppearanceStream("/N"); + if (AS.isNull()) + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper create AS from scratch"); + QPDFObjectHandle::Rectangle rect = aoh.getRect(); + QPDFObjectHandle::Rectangle bbox( + 0, 0, rect.urx - rect.llx, rect.ury - rect.lly); + QPDFObjectHandle dict = QPDFObjectHandle::parse( + "<< /Resources << /ProcSet [ /PDF /Text ] >>" + " /Type /XObject /Subtype /Form >>"); + dict.replaceKey("/BBox", QPDFObjectHandle::newFromRectangle(bbox)); + AS = QPDFObjectHandle::newStream( + this->oh.getOwningQPDF(), "/Tx BMC\nEMC\n"); + AS.replaceDict(dict); + QPDFObjectHandle AP = aoh.getAppearanceDictionary(); + if (AP.isNull()) + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper create AP from scratch"); + aoh.getObjectHandle().replaceKey( + "/AP", QPDFObjectHandle::newDictionary()); + AP = aoh.getAppearanceDictionary(); + } + AP.replaceKey("/N", AS); + } + if (! AS.isStream()) + { + aoh.getObjectHandle().warnIfPossible( + "unable to get normal appearance stream for update"); + return; + } + QPDFObjectHandle bbox_obj = AS.getDict().getKey("/BBox"); + if (! bbox_obj.isRectangle()) + { + aoh.getObjectHandle().warnIfPossible( + "unable to get appearance stream bounding box"); + return; + } + QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle(); + std::string DA = getDefaultAppearance(); + std::string V = getValueAsString(); + std::vector<std::string> opt; + if (isChoice() && ((getFlags() & ff_ch_combo) == 0)) + { + opt = getChoices(); + } + + TfFinder tff; + Pl_QPDFTokenizer tok("tf", &tff); + tok.write(QUtil::unsigned_char_pointer(DA.c_str()), DA.length()); + tok.finish(); + double tf = tff.getTf(); + DA = tff.getDA(); + + std::string (*encoder)(std::string const&, char) = &QUtil::utf8_to_ascii; + std::string font_name = tff.getFontName(); + if (! font_name.empty()) + { + // See if the font is encoded with something we know about. + QPDFObjectHandle resources = AS.getDict().getKey("/Resources"); + QPDFObjectHandle font = getFontFromResource(resources, font_name); + if (! font.isInitialized()) + { + QPDFObjectHandle dr = getInheritableFieldValue("/DR"); + font = getFontFromResource(dr, font_name); + } + if (font.isDictionary() && + font.getKey("/Encoding").isName()) + { + std::string encoding = font.getKey("/Encoding").getName(); + if (encoding == "/WinAnsiEncoding") + { + QTC::TC("qpdf", "QPDFFormFieldObjectHelper WinAnsi"); + encoder = &QUtil::utf8_to_win_ansi; + } + else if (encoding == "/MacRomanEncoding") + { + encoder = &QUtil::utf8_to_mac_roman; + } + } + } + + V = (*encoder)(V, '?'); + for (size_t i = 0; i < opt.size(); ++i) + { + opt.at(i) = (*encoder)(opt.at(i), '?'); + } + + AS.addTokenFilter(new ValueSetter(DA, V, opt, tf, bbox)); +} |