Commit b55567a0fa6708500cd0905f7a26a28d70979001
1 parent
13426123
Add special case setV code for button fields
Showing
11 changed files
with
15458 additions
and
5 deletions
include/qpdf/QPDFFormFieldObjectHelper.hh
| ... | ... | @@ -163,10 +163,13 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper |
| 163 | 163 | void setFieldAttribute(std::string const& key, |
| 164 | 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 | 173 | QPDF_DLL |
| 171 | 174 | void setV(QPDFObjectHandle value, bool need_appearances = true); |
| 172 | 175 | |
| ... | ... | @@ -177,6 +180,9 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper |
| 177 | 180 | void setV(std::string const& utf8_value, bool need_appearances = true); |
| 178 | 181 | |
| 179 | 182 | private: |
| 183 | + void setRadioButtonValue(QPDFObjectHandle name); | |
| 184 | + void setCheckBoxValue(bool value); | |
| 185 | + | |
| 180 | 186 | class Members |
| 181 | 187 | { |
| 182 | 188 | friend class QPDFFormFieldObjectHelper; | ... | ... |
libqpdf/QPDFFormFieldObjectHelper.cc
| ... | ... | @@ -272,6 +272,47 @@ void |
| 272 | 272 | QPDFFormFieldObjectHelper::setV( |
| 273 | 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 | 316 | setFieldAttribute("/V", value); |
| 276 | 317 | if (need_appearances) |
| 277 | 318 | { |
| ... | ... | @@ -294,3 +335,138 @@ QPDFFormFieldObjectHelper::setV( |
| 294 | 335 | setV(QPDFObjectHandle::newUnicodeString(utf8_value), |
| 295 | 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 | 390 | QPDFPageDocumentHelper indirect as resources 0 |
| 391 | 391 | QPDFAnnotationObjectHelper forbidden flags 0 |
| 392 | 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 | 188 | 'form-errors', |
| 189 | 189 | ); |
| 190 | 190 | |
| 191 | -$n_tests += scalar(@form_tests) + 2; | |
| 191 | +$n_tests += scalar(@form_tests) + 6; | |
| 192 | 192 | |
| 193 | 193 | # Many of the form*.pdf files were created by converting the |
| 194 | 194 | # LibreOffice document storage/form.odt to PDF and then manually |
| ... | ... | @@ -216,6 +216,22 @@ $td->runtest("compare files", |
| 216 | 216 | {$td->FILE => "a.pdf"}, |
| 217 | 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 | 235 | show_ntests(); |
| 220 | 236 | # ---------- |
| 221 | 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
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 | 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 | 1823 | else |
| 1775 | 1824 | { |
| 1776 | 1825 | throw std::runtime_error(std::string("invalid test ") + | ... | ... |