aboutsummaryrefslogtreecommitdiffstats
path: root/libqpdf/QPDFFormFieldObjectHelper.cc
diff options
context:
space:
mode:
Diffstat (limited to 'libqpdf/QPDFFormFieldObjectHelper.cc')
-rw-r--r--libqpdf/QPDFFormFieldObjectHelper.cc686
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));
+}