Commit b55567a0fa6708500cd0905f7a26a28d70979001

Authored by Jay Berkenbilt
1 parent 13426123

Add special case setV code for button fields

include/qpdf/QPDFFormFieldObjectHelper.hh
@@ -163,10 +163,13 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper @@ -163,10 +163,13 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper
163 void setFieldAttribute(std::string const& key, 163 void setFieldAttribute(std::string const& key,
164 std::string const& utf8_value); 164 std::string const& utf8_value);
165 165
166 - // Set /V (field value) to the given value. Optionally set  
167 - // /NeedAppearances to true. You can explicitly tell this method  
168 - // not to set /NeedAppearances if you are going to explicitly  
169 - // generate an appearance stream yourself. 166 + // Set /V (field value) to the given value. If need_appearances is
  167 + // true and the field type is either /Tx (text) or /Ch (choice),
  168 + // set /NeedAppearances to true. You can explicitly tell this
  169 + // method not to set /NeedAppearances if you are going to generate
  170 + // an appearance stream yourself. Starting with qpdf 8.3.0, this
  171 + // method handles fields of type /Btn (checkboxes, radio buttons,
  172 + // pushbuttons) specially.
170 QPDF_DLL 173 QPDF_DLL
171 void setV(QPDFObjectHandle value, bool need_appearances = true); 174 void setV(QPDFObjectHandle value, bool need_appearances = true);
172 175
@@ -177,6 +180,9 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper @@ -177,6 +180,9 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper
177 void setV(std::string const& utf8_value, bool need_appearances = true); 180 void setV(std::string const& utf8_value, bool need_appearances = true);
178 181
179 private: 182 private:
  183 + void setRadioButtonValue(QPDFObjectHandle name);
  184 + void setCheckBoxValue(bool value);
  185 +
180 class Members 186 class Members
181 { 187 {
182 friend class QPDFFormFieldObjectHelper; 188 friend class QPDFFormFieldObjectHelper;
libqpdf/QPDFFormFieldObjectHelper.cc
@@ -272,6 +272,47 @@ void @@ -272,6 +272,47 @@ void
272 QPDFFormFieldObjectHelper::setV( 272 QPDFFormFieldObjectHelper::setV(
273 QPDFObjectHandle value, bool need_appearances) 273 QPDFObjectHandle value, bool need_appearances)
274 { 274 {
  275 + if (getFieldType() == "/Btn")
  276 + {
  277 + if (isCheckbox())
  278 + {
  279 + bool okay = false;
  280 + if (value.isName())
  281 + {
  282 + std::string name = value.getName();
  283 + if ((name == "/Yes") || (name == "/Off"))
  284 + {
  285 + okay = true;
  286 + setCheckBoxValue((name == "/Yes"));
  287 + }
  288 + }
  289 + if (! okay)
  290 + {
  291 + this->oh.warnIfPossible(
  292 + "ignoring attempt to set a checkbox field to a"
  293 + " value of other than /Yes or /Off");
  294 + }
  295 + }
  296 + else if (isRadioButton())
  297 + {
  298 + if (value.isName())
  299 + {
  300 + setRadioButtonValue(value);
  301 + }
  302 + else
  303 + {
  304 + this->oh.warnIfPossible(
  305 + "ignoring attempt to set a radio button field to"
  306 + " an object that is not a name");
  307 + }
  308 + }
  309 + else if (isPushbutton())
  310 + {
  311 + this->oh.warnIfPossible(
  312 + "ignoring attempt set the value of a pushbutton field");
  313 + }
  314 + return;
  315 + }
275 setFieldAttribute("/V", value); 316 setFieldAttribute("/V", value);
276 if (need_appearances) 317 if (need_appearances)
277 { 318 {
@@ -294,3 +335,138 @@ QPDFFormFieldObjectHelper::setV( @@ -294,3 +335,138 @@ QPDFFormFieldObjectHelper::setV(
294 setV(QPDFObjectHandle::newUnicodeString(utf8_value), 335 setV(QPDFObjectHandle::newUnicodeString(utf8_value),
295 need_appearances); 336 need_appearances);
296 } 337 }
  338 +
  339 +void
  340 +QPDFFormFieldObjectHelper::setRadioButtonValue(QPDFObjectHandle name)
  341 +{
  342 + // Set the value of a radio button field. This has the following
  343 + // specific behavior:
  344 + // * If this is a radio button field that has a parent that is
  345 + // also a radio button field and has no explicit /V, call itself
  346 + // on the parent
  347 + // * If this is a radio button field with childen, set /V to the
  348 + // given value. Then, for each child, if the child has the
  349 + // specified value as one of its keys in the /N subdictionary of
  350 + // its /AP (i.e. its normal appearance stream dictionary), set
  351 + // /AS to name; otherwise, if /Off is a member, set /AS to /Off.
  352 + // Note that we never turn on /NeedAppearances when setting a
  353 + // radio button field.
  354 + QPDFObjectHandle parent = this->oh.getKey("/Parent");
  355 + if (parent.isDictionary() && parent.getKey("/Parent").isNull())
  356 + {
  357 + QPDFFormFieldObjectHelper ph(parent);
  358 + if (ph.isRadioButton())
  359 + {
  360 + // This is most likely one of the individual buttons. Try
  361 + // calling on the parent.
  362 + QTC::TC("qpdf", "QPDFFormFieldObjectHelper set parent radio button");
  363 + ph.setRadioButtonValue(name);
  364 + return;
  365 + }
  366 + }
  367 +
  368 + QPDFObjectHandle kids = this->oh.getKey("/Kids");
  369 + if (! (isRadioButton() && parent.isNull() && kids.isArray()))
  370 + {
  371 + this->oh.warnIfPossible("don't know how to set the value"
  372 + " of this field as a radio button");
  373 + return;
  374 + }
  375 + setFieldAttribute("/V", name);
  376 + int nkids = kids.getArrayNItems();
  377 + for (int i = 0; i < nkids; ++i)
  378 + {
  379 + QPDFObjectHandle kid = kids.getArrayItem(i);
  380 + QPDFObjectHandle AP = kid.getKey("/AP");
  381 + QPDFObjectHandle annot;
  382 + if (AP.isNull())
  383 + {
  384 + // The widget may be below. If there is more than one,
  385 + // just find the first one.
  386 + QPDFObjectHandle grandkids = kid.getKey("/Kids");
  387 + if (grandkids.isArray())
  388 + {
  389 + int ngrandkids = grandkids.getArrayNItems();
  390 + for (int j = 0; j < ngrandkids; ++j)
  391 + {
  392 + QPDFObjectHandle grandkid = grandkids.getArrayItem(j);
  393 + AP = grandkid.getKey("/AP");
  394 + if (! AP.isNull())
  395 + {
  396 + QTC::TC("qpdf", "QPDFFormFieldObjectHelper radio button grandkid widget");
  397 + annot = grandkid;
  398 + break;
  399 + }
  400 + }
  401 + }
  402 + }
  403 + else
  404 + {
  405 + annot = kid;
  406 + }
  407 + if (! annot.isInitialized())
  408 + {
  409 + QTC::TC("qpdf", "QPDFObjectHandle broken radio button");
  410 + this->oh.warnIfPossible(
  411 + "unable to set the value of this radio button");
  412 + continue;
  413 + }
  414 + if (AP.isDictionary() &&
  415 + AP.getKey("/N").isDictionary() &&
  416 + AP.getKey("/N").hasKey(name.getName()))
  417 + {
  418 + QTC::TC("qpdf", "QPDFFormFieldObjectHelper turn on radio button");
  419 + annot.replaceKey("/AS", name);
  420 + }
  421 + else
  422 + {
  423 + QTC::TC("qpdf", "QPDFFormFieldObjectHelper turn off radio button");
  424 + annot.replaceKey("/AS", QPDFObjectHandle::newName("/Off"));
  425 + }
  426 + }
  427 +}
  428 +
  429 +void
  430 +QPDFFormFieldObjectHelper::setCheckBoxValue(bool value)
  431 +{
  432 + // Set /AS to /Yes or /Off in addition to setting /V.
  433 + QPDFObjectHandle name =
  434 + QPDFObjectHandle::newName(value ? "/Yes" : "/Off");
  435 + setFieldAttribute("/V", name);
  436 + QPDFObjectHandle AP = this->oh.getKey("/AP");
  437 + QPDFObjectHandle annot;
  438 + if (AP.isNull())
  439 + {
  440 + // The widget may be below. If there is more than one, just
  441 + // find the first one.
  442 + QPDFObjectHandle kids = this->oh.getKey("/Kids");
  443 + if (kids.isArray())
  444 + {
  445 + int nkids = kids.getArrayNItems();
  446 + for (int i = 0; i < nkids; ++i)
  447 + {
  448 + QPDFObjectHandle kid = kids.getArrayItem(i);
  449 + AP = kid.getKey("/AP");
  450 + if (! AP.isNull())
  451 + {
  452 + QTC::TC("qpdf", "QPDFFormFieldObjectHelper checkbox kid widget");
  453 + annot = kid;
  454 + break;
  455 + }
  456 + }
  457 + }
  458 + }
  459 + else
  460 + {
  461 + annot = this->oh;
  462 + }
  463 + if (! annot.isInitialized())
  464 + {
  465 + QTC::TC("qpdf", "QPDFObjectHandle broken checkbox");
  466 + this->oh.warnIfPossible(
  467 + "unable to set the value of this checkbox");
  468 + return;
  469 + }
  470 + QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS");
  471 + annot.replaceKey("/AS", name);
  472 +}
qpdf/qpdf.testcov
@@ -390,3 +390,11 @@ QPDFObjectHandle replace with copy 0 @@ -390,3 +390,11 @@ QPDFObjectHandle replace with copy 0
390 QPDFPageDocumentHelper indirect as resources 0 390 QPDFPageDocumentHelper indirect as resources 0
391 QPDFAnnotationObjectHelper forbidden flags 0 391 QPDFAnnotationObjectHelper forbidden flags 0
392 QPDFAnnotationObjectHelper missing required flags 0 392 QPDFAnnotationObjectHelper missing required flags 0
  393 +QPDFFormFieldObjectHelper set parent radio button 0
  394 +QPDFFormFieldObjectHelper radio button grandkid widget 0
  395 +QPDFFormFieldObjectHelper turn on radio button 0
  396 +QPDFFormFieldObjectHelper turn off radio button 0
  397 +QPDFFormFieldObjectHelper checkbox kid widget 0
  398 +QPDFObjectHandle broken radio button 0
  399 +QPDFFormFieldObjectHelper set checkbox AS 0
  400 +QPDFObjectHandle broken checkbox 0
qpdf/qtest/qpdf.test
@@ -188,7 +188,7 @@ my @form_tests = ( @@ -188,7 +188,7 @@ my @form_tests = (
188 'form-errors', 188 'form-errors',
189 ); 189 );
190 190
191 -$n_tests += scalar(@form_tests) + 2; 191 +$n_tests += scalar(@form_tests) + 6;
192 192
193 # Many of the form*.pdf files were created by converting the 193 # Many of the form*.pdf files were created by converting the
194 # LibreOffice document storage/form.odt to PDF and then manually 194 # LibreOffice document storage/form.odt to PDF and then manually
@@ -216,6 +216,22 @@ $td-&gt;runtest(&quot;compare files&quot;, @@ -216,6 +216,22 @@ $td-&gt;runtest(&quot;compare files&quot;,
216 {$td->FILE => "a.pdf"}, 216 {$td->FILE => "a.pdf"},
217 {$td->FILE => "form-no-need-appearances-filled.pdf"}); 217 {$td->FILE => "form-no-need-appearances-filled.pdf"});
218 218
  219 +$td->runtest("button fields",
  220 + {$td->COMMAND => "test_driver 51 button-set.pdf"},
  221 + {$td->FILE => "button-set.out", $td->EXIT_STATUS => 0},
  222 + $td->NORMALIZE_NEWLINES);
  223 +$td->runtest("compare files",
  224 + {$td->FILE => "a.pdf"},
  225 + {$td->FILE => "button-set-out.pdf"});
  226 +
  227 +$td->runtest("broken button fields",
  228 + {$td->COMMAND => "test_driver 51 button-set-broken.pdf"},
  229 + {$td->FILE => "button-set-broken.out", $td->EXIT_STATUS => 0},
  230 + $td->NORMALIZE_NEWLINES);
  231 +$td->runtest("compare files",
  232 + {$td->FILE => "a.pdf"},
  233 + {$td->FILE => "button-set-broken-out.pdf"});
  234 +
219 show_ntests(); 235 show_ntests();
220 # ---------- 236 # ----------
221 $td->notify("--- Stream Replacement Tests ---"); 237 $td->notify("--- Stream Replacement Tests ---");
qpdf/qtest/qpdf/button-set-broken-out.pdf 0 → 100644
No preview for this file type
qpdf/qtest/qpdf/button-set-broken.out 0 → 100644
  1 +setting r1 via parent
  2 +WARNING: button-set-broken.pdf, object 5 0 at offset 995: unable to set the value of this radio button
  3 +turning checkbox1 on
  4 +turning checkbox2 off
  5 +WARNING: button-set-broken.pdf, object 7 0 at offset 1354: unable to set the value of this checkbox
  6 +setting r2 via child
  7 +test 51 done
qpdf/qtest/qpdf/button-set-broken.pdf 0 → 100644
No preview for this file type
qpdf/qtest/qpdf/button-set-out.pdf 0 → 100644
No preview for this file type
qpdf/qtest/qpdf/button-set.out 0 → 100644
  1 +setting r1 via parent
  2 +turning checkbox1 on
  3 +turning checkbox2 off
  4 +setting r2 via child
  5 +test 51 done
qpdf/qtest/qpdf/button-set.pdf 0 → 100644
No preview for this file type
qpdf/test_driver.cc
@@ -1771,6 +1771,55 @@ void runtest(int n, char const* filename1, char const* arg2) @@ -1771,6 +1771,55 @@ void runtest(int n, char const* filename1, char const* arg2)
1771 std::cout << *iter << std::endl; 1771 std::cout << *iter << std::endl;
1772 } 1772 }
1773 } 1773 }
  1774 + else if (n == 51)
  1775 + {
  1776 + // Test radio button and checkbox field setting. The input
  1777 + // files must have radios button called r1 and r2 and
  1778 + // checkboxes called checkbox1 and checkbox2. The files
  1779 + // button-set*.pdf are designed for this test case.
  1780 + QPDFObjectHandle acroform = pdf.getRoot().getKey("/AcroForm");
  1781 + QPDFObjectHandle fields = acroform.getKey("/Fields");
  1782 + int n = fields.getArrayNItems();
  1783 + for (int i = 0; i < n; ++i)
  1784 + {
  1785 + QPDFObjectHandle field = fields.getArrayItem(i);
  1786 + QPDFObjectHandle T = field.getKey("/T");
  1787 + if (! T.isString())
  1788 + {
  1789 + continue;
  1790 + }
  1791 + std::string Tval = T.getUTF8Value();
  1792 + if (Tval == "r1")
  1793 + {
  1794 + std::cout << "setting r1 via parent\n";
  1795 + QPDFFormFieldObjectHelper foh(field);
  1796 + foh.setV(QPDFObjectHandle::newName("/2"));
  1797 + }
  1798 + else if (Tval == "r2")
  1799 + {
  1800 + std::cout << "setting r2 via child\n";
  1801 + field = field.getKey("/Kids").getArrayItem(1);
  1802 + QPDFFormFieldObjectHelper foh(field);
  1803 + foh.setV(QPDFObjectHandle::newName("/3"));
  1804 + }
  1805 + else if (Tval == "checkbox1")
  1806 + {
  1807 + std::cout << "turning checkbox1 on\n";
  1808 + QPDFFormFieldObjectHelper foh(field);
  1809 + foh.setV(QPDFObjectHandle::newName("/Yes"));
  1810 + }
  1811 + else if (Tval == "checkbox2")
  1812 + {
  1813 + std::cout << "turning checkbox2 off\n";
  1814 + QPDFFormFieldObjectHelper foh(field);
  1815 + foh.setV(QPDFObjectHandle::newName("/Off"));
  1816 + }
  1817 + }
  1818 + QPDFWriter w(pdf, "a.pdf");
  1819 + w.setQDFMode(true);
  1820 + w.setStaticID(true);
  1821 + w.write();
  1822 + }
1774 else 1823 else
1775 { 1824 {
1776 throw std::runtime_error(std::string("invalid test ") + 1825 throw std::runtime_error(std::string("invalid test ") +