Commit 158156d5062a5ac335bcfde7893be4671affdc32
1 parent
02281632
Add basic appearance stream generation
Showing
20 changed files
with
31076 additions
and
394 deletions
ChangeLog
| 1 | 2019-01-03 Jay Berkenbilt <ejb@ql.org> | 1 | 2019-01-03 Jay Berkenbilt <ejb@ql.org> |
| 2 | 2 | ||
| 3 | + * Fix behavior of form field value setting to handle the following | ||
| 4 | + cases: | ||
| 5 | + - Strings are always written as UTF-16 | ||
| 6 | + - Check boxes and radio buttons are handled properly with | ||
| 7 | + synchronization of values and appearance states | ||
| 8 | + | ||
| 9 | + * Define constants in qpdf/Constants.h for interpretation of | ||
| 10 | + annotation and form field flags | ||
| 11 | + | ||
| 12 | + * Add QPDFAnnotationObjectHelper::getFlags | ||
| 13 | + | ||
| 14 | + * Add many new methods to QPDFFormFieldObjectHelper for querying | ||
| 15 | + flags and field types | ||
| 16 | + | ||
| 17 | + * Add new methods for appearance stream generation. See comments | ||
| 18 | + in QPDFFormFieldObjectHelper.hh for generateAppearance() for a | ||
| 19 | + description of limitations. | ||
| 20 | + - QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded | ||
| 21 | + - QPDFFormFieldObjectHelper::generateAppearance | ||
| 22 | + | ||
| 23 | + * Bug fix: when writing form field values, always write string | ||
| 24 | + values encoded as UTF-16. | ||
| 25 | + | ||
| 3 | * Add method QUtil::utf8_to_ascii, which returns an ASCII string | 26 | * Add method QUtil::utf8_to_ascii, which returns an ASCII string |
| 4 | for a UTF-8 string, replacing out-of-range characters with a | 27 | for a UTF-8 string, replacing out-of-range characters with a |
| 5 | specified substitute. | 28 | specified substitute. |
include/qpdf/QPDFAcroFormDocumentHelper.hh
| @@ -157,6 +157,16 @@ class QPDFAcroFormDocumentHelper: public QPDFDocumentHelper | @@ -157,6 +157,16 @@ class QPDFAcroFormDocumentHelper: public QPDFDocumentHelper | ||
| 157 | QPDF_DLL | 157 | QPDF_DLL |
| 158 | void setNeedAppearances(bool); | 158 | void setNeedAppearances(bool); |
| 159 | 159 | ||
| 160 | + // If /NeedAppearances is false, do nothing. Otherwise generate | ||
| 161 | + // appearance streams for all widget annotations that need them. | ||
| 162 | + // See comments in QPDFFormFieldObjectHelper.hh for | ||
| 163 | + // generateAppearance for limitations. For checkbox and radio | ||
| 164 | + // button fields, this code ensures that appearance state is | ||
| 165 | + // consistent with the field's value and uses any pre-existing | ||
| 166 | + // appearance streams. | ||
| 167 | + QPDF_DLL | ||
| 168 | + void generateAppearancesIfNeeded(); | ||
| 169 | + | ||
| 160 | private: | 170 | private: |
| 161 | void analyze(); | 171 | void analyze(); |
| 162 | void traverseField(QPDFObjectHandle field, | 172 | void traverseField(QPDFObjectHandle field, |
include/qpdf/QPDFFormFieldObjectHelper.hh
| @@ -31,6 +31,8 @@ | @@ -31,6 +31,8 @@ | ||
| 31 | #include <qpdf/DLL.h> | 31 | #include <qpdf/DLL.h> |
| 32 | #include <vector> | 32 | #include <vector> |
| 33 | 33 | ||
| 34 | +class QPDFAnnotationObjectHelper; | ||
| 35 | + | ||
| 34 | class QPDFFormFieldObjectHelper: public QPDFObjectHelper | 36 | class QPDFFormFieldObjectHelper: public QPDFObjectHelper |
| 35 | { | 37 | { |
| 36 | public: | 38 | public: |
| @@ -179,9 +181,21 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper | @@ -179,9 +181,21 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper | ||
| 179 | QPDF_DLL | 181 | QPDF_DLL |
| 180 | void setV(std::string const& utf8_value, bool need_appearances = true); | 182 | void setV(std::string const& utf8_value, bool need_appearances = true); |
| 181 | 183 | ||
| 184 | + // Update the appearance stream for this field. Note that qpdf's | ||
| 185 | + // abilitiy to generate appearance streams is limited. We only | ||
| 186 | + // generate appearance streams for streams of type text or choice. | ||
| 187 | + // The appearance uses the default parameters provided in the | ||
| 188 | + // file, and it only supports ASCII characters. Quadding is | ||
| 189 | + // currently ignored. While this functionality is limited, it | ||
| 190 | + // should do a decent job on properly constructed PDF files when | ||
| 191 | + // field values are restricted to ASCII characters. | ||
| 192 | + QPDF_DLL | ||
| 193 | + void generateAppearance(QPDFAnnotationObjectHelper&); | ||
| 194 | + | ||
| 182 | private: | 195 | private: |
| 183 | void setRadioButtonValue(QPDFObjectHandle name); | 196 | void setRadioButtonValue(QPDFObjectHandle name); |
| 184 | void setCheckBoxValue(bool value); | 197 | void setCheckBoxValue(bool value); |
| 198 | + void generateTextAppearance(QPDFAnnotationObjectHelper&); | ||
| 185 | 199 | ||
| 186 | class Members | 200 | class Members |
| 187 | { | 201 | { |
libqpdf/QPDFAcroFormDocumentHelper.cc
| @@ -285,3 +285,48 @@ QPDFAcroFormDocumentHelper::setNeedAppearances(bool val) | @@ -285,3 +285,48 @@ QPDFAcroFormDocumentHelper::setNeedAppearances(bool val) | ||
| 285 | acroform.removeKey("/NeedAppearances"); | 285 | acroform.removeKey("/NeedAppearances"); |
| 286 | } | 286 | } |
| 287 | } | 287 | } |
| 288 | + | ||
| 289 | +void | ||
| 290 | +QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded() | ||
| 291 | +{ | ||
| 292 | + if (! getNeedAppearances()) | ||
| 293 | + { | ||
| 294 | + return; | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + QPDFPageDocumentHelper pdh(this->qpdf); | ||
| 298 | + std::vector<QPDFPageObjectHelper> pages = pdh.getAllPages(); | ||
| 299 | + for (std::vector<QPDFPageObjectHelper>::iterator page_iter = | ||
| 300 | + pages.begin(); | ||
| 301 | + page_iter != pages.end(); ++page_iter) | ||
| 302 | + { | ||
| 303 | + std::vector<QPDFAnnotationObjectHelper> annotations = | ||
| 304 | + getWidgetAnnotationsForPage(*page_iter); | ||
| 305 | + for (std::vector<QPDFAnnotationObjectHelper>::iterator annot_iter = | ||
| 306 | + annotations.begin(); | ||
| 307 | + annot_iter != annotations.end(); ++annot_iter) | ||
| 308 | + { | ||
| 309 | + QPDFAnnotationObjectHelper& aoh = *annot_iter; | ||
| 310 | + QPDFFormFieldObjectHelper ffh = | ||
| 311 | + getFieldForAnnotation(aoh); | ||
| 312 | + if (ffh.getFieldType() == "/Btn") | ||
| 313 | + { | ||
| 314 | + // Rather than generating appearances for button | ||
| 315 | + // fields, rely on what's already there. Just make | ||
| 316 | + // sure /AS is consistent with /V, which we can do by | ||
| 317 | + // resetting the value of the field back to itself. | ||
| 318 | + // This code is referenced in a comment in | ||
| 319 | + // QPDFFormFieldObjectHelper::generateAppearance. | ||
| 320 | + if (ffh.isRadioButton() || ffh.isCheckbox()) | ||
| 321 | + { | ||
| 322 | + ffh.setV(ffh.getValue()); | ||
| 323 | + } | ||
| 324 | + } | ||
| 325 | + else | ||
| 326 | + { | ||
| 327 | + ffh.generateAppearance(aoh); | ||
| 328 | + } | ||
| 329 | + } | ||
| 330 | + } | ||
| 331 | + setNeedAppearances(false); | ||
| 332 | +} |
libqpdf/QPDFFormFieldObjectHelper.cc
| 1 | #include <qpdf/QPDFFormFieldObjectHelper.hh> | 1 | #include <qpdf/QPDFFormFieldObjectHelper.hh> |
| 2 | #include <qpdf/QTC.hh> | 2 | #include <qpdf/QTC.hh> |
| 3 | #include <qpdf/QPDFAcroFormDocumentHelper.hh> | 3 | #include <qpdf/QPDFAcroFormDocumentHelper.hh> |
| 4 | +#include <qpdf/QPDFAnnotationObjectHelper.hh> | ||
| 5 | +#include <qpdf/QUtil.hh> | ||
| 6 | +#include <qpdf/Pl_QPDFTokenizer.hh> | ||
| 7 | +#include <stdlib.h> | ||
| 4 | 8 | ||
| 5 | QPDFFormFieldObjectHelper::Members::~Members() | 9 | QPDFFormFieldObjectHelper::Members::~Members() |
| 6 | { | 10 | { |
| @@ -313,7 +317,15 @@ QPDFFormFieldObjectHelper::setV( | @@ -313,7 +317,15 @@ QPDFFormFieldObjectHelper::setV( | ||
| 313 | } | 317 | } |
| 314 | return; | 318 | return; |
| 315 | } | 319 | } |
| 316 | - setFieldAttribute("/V", value); | 320 | + if (value.isString()) |
| 321 | + { | ||
| 322 | + setFieldAttribute( | ||
| 323 | + "/V", QPDFObjectHandle::newUnicodeString(value.getUTF8Value())); | ||
| 324 | + } | ||
| 325 | + else | ||
| 326 | + { | ||
| 327 | + setFieldAttribute("/V", value); | ||
| 328 | + } | ||
| 317 | if (need_appearances) | 329 | if (need_appearances) |
| 318 | { | 330 | { |
| 319 | QPDF* qpdf = this->oh.getOwningQPDF(); | 331 | QPDF* qpdf = this->oh.getOwningQPDF(); |
| @@ -470,3 +482,290 @@ QPDFFormFieldObjectHelper::setCheckBoxValue(bool value) | @@ -470,3 +482,290 @@ QPDFFormFieldObjectHelper::setCheckBoxValue(bool value) | ||
| 470 | QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS"); | 482 | QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS"); |
| 471 | annot.replaceKey("/AS", name); | 483 | annot.replaceKey("/AS", name); |
| 472 | } | 484 | } |
| 485 | + | ||
| 486 | +void | ||
| 487 | +QPDFFormFieldObjectHelper::generateAppearance(QPDFAnnotationObjectHelper& aoh) | ||
| 488 | +{ | ||
| 489 | + std::string ft = getFieldType(); | ||
| 490 | + // Ignore field types we don't know how to generate appearances | ||
| 491 | + // for. Button fields don't really need them -- see code in | ||
| 492 | + // QPDFAcroFormDocumentHelper::generateAppearancesIfNeeded. | ||
| 493 | + if ((ft == "/Tx") || (ft == "/Ch")) | ||
| 494 | + { | ||
| 495 | + generateTextAppearance(aoh); | ||
| 496 | + } | ||
| 497 | +} | ||
| 498 | + | ||
| 499 | +class ValueSetter: public QPDFObjectHandle::TokenFilter | ||
| 500 | +{ | ||
| 501 | + public: | ||
| 502 | + ValueSetter(std::string const& DA, std::string const& V, | ||
| 503 | + std::vector<std::string> const& opt, double tf, | ||
| 504 | + QPDFObjectHandle::Rectangle const& bbox); | ||
| 505 | + virtual ~ValueSetter() | ||
| 506 | + { | ||
| 507 | + } | ||
| 508 | + virtual void handleToken(QPDFTokenizer::Token const&); | ||
| 509 | + void writeAppearance(); | ||
| 510 | + | ||
| 511 | + private: | ||
| 512 | + std::string DA; | ||
| 513 | + std::string V; | ||
| 514 | + std::vector<std::string> opt; | ||
| 515 | + double tf; | ||
| 516 | + QPDFObjectHandle::Rectangle bbox; | ||
| 517 | + enum { st_top, st_bmc, st_emc, st_end } state; | ||
| 518 | +}; | ||
| 519 | + | ||
| 520 | +ValueSetter::ValueSetter(std::string const& DA, std::string const& V, | ||
| 521 | + std::vector<std::string> const& opt, double tf, | ||
| 522 | + QPDFObjectHandle::Rectangle const& bbox) : | ||
| 523 | + DA(DA), | ||
| 524 | + V(V), | ||
| 525 | + opt(opt), | ||
| 526 | + tf(tf), | ||
| 527 | + bbox(bbox), | ||
| 528 | + state(st_top) | ||
| 529 | +{ | ||
| 530 | +} | ||
| 531 | + | ||
| 532 | +void | ||
| 533 | +ValueSetter::handleToken(QPDFTokenizer::Token const& token) | ||
| 534 | +{ | ||
| 535 | + QPDFTokenizer::token_type_e ttype = token.getType(); | ||
| 536 | + std::string value = token.getValue(); | ||
| 537 | + bool do_replace = false; | ||
| 538 | + switch (state) | ||
| 539 | + { | ||
| 540 | + case st_top: | ||
| 541 | + writeToken(token); | ||
| 542 | + if ((ttype == QPDFTokenizer::tt_word) && (value == "BMC")) | ||
| 543 | + { | ||
| 544 | + state = st_bmc; | ||
| 545 | + } | ||
| 546 | + break; | ||
| 547 | + | ||
| 548 | + case st_bmc: | ||
| 549 | + if ((ttype == QPDFTokenizer::tt_space) || | ||
| 550 | + (ttype == QPDFTokenizer::tt_comment)) | ||
| 551 | + { | ||
| 552 | + writeToken(token); | ||
| 553 | + } | ||
| 554 | + else | ||
| 555 | + { | ||
| 556 | + state = st_emc; | ||
| 557 | + } | ||
| 558 | + // fall through to emc | ||
| 559 | + | ||
| 560 | + case st_emc: | ||
| 561 | + if ((ttype == QPDFTokenizer::tt_word) && (value == "EMC")) | ||
| 562 | + { | ||
| 563 | + do_replace = true; | ||
| 564 | + state = st_end; | ||
| 565 | + } | ||
| 566 | + break; | ||
| 567 | + | ||
| 568 | + case st_end: | ||
| 569 | + writeToken(token); | ||
| 570 | + break; | ||
| 571 | + } | ||
| 572 | + if (do_replace) | ||
| 573 | + { | ||
| 574 | + writeAppearance(); | ||
| 575 | + } | ||
| 576 | +} | ||
| 577 | + | ||
| 578 | +void ValueSetter::writeAppearance() | ||
| 579 | +{ | ||
| 580 | + // This code does not take quadding into consideration because | ||
| 581 | + // doing so requires font metric information, which we don't | ||
| 582 | + // have in many cases. | ||
| 583 | + | ||
| 584 | + double tfh = 1.2 * tf; | ||
| 585 | + int dx = 1; | ||
| 586 | + | ||
| 587 | + // Write one or more lines, centered vertically, possibly with | ||
| 588 | + // one row highlighted. | ||
| 589 | + | ||
| 590 | + size_t max_rows = static_cast<size_t>((bbox.ury - bbox.lly) / tfh); | ||
| 591 | + bool highlight = false; | ||
| 592 | + size_t highlight_idx = 0; | ||
| 593 | + | ||
| 594 | + std::vector<std::string> lines; | ||
| 595 | + if (opt.empty() || (max_rows < 2)) | ||
| 596 | + { | ||
| 597 | + lines.push_back(V); | ||
| 598 | + } | ||
| 599 | + else | ||
| 600 | + { | ||
| 601 | + // Figure out what rows to write | ||
| 602 | + size_t nopt = opt.size(); | ||
| 603 | + size_t found_idx = 0; | ||
| 604 | + bool found = false; | ||
| 605 | + for (found_idx = 0; found_idx < nopt; ++found_idx) | ||
| 606 | + { | ||
| 607 | + if (opt.at(found_idx) == V) | ||
| 608 | + { | ||
| 609 | + found = true; | ||
| 610 | + break; | ||
| 611 | + } | ||
| 612 | + } | ||
| 613 | + if (found) | ||
| 614 | + { | ||
| 615 | + // Try to make the found item the second one, but | ||
| 616 | + // adjust for under/overflow. | ||
| 617 | + int wanted_first = found_idx - 1; | ||
| 618 | + int wanted_last = found_idx + max_rows - 2; | ||
| 619 | + QTC::TC("qpdf", "QPDFFormFieldObjectHelper list found"); | ||
| 620 | + while (wanted_first < 0) | ||
| 621 | + { | ||
| 622 | + QTC::TC("qpdf", "QPDFFormFieldObjectHelper list first too low"); | ||
| 623 | + ++wanted_first; | ||
| 624 | + ++wanted_last; | ||
| 625 | + } | ||
| 626 | + while (wanted_last >= static_cast<int>(nopt)) | ||
| 627 | + { | ||
| 628 | + QTC::TC("qpdf", "QPDFFormFieldObjectHelper list last too high"); | ||
| 629 | + if (wanted_first > 0) | ||
| 630 | + { | ||
| 631 | + --wanted_first; | ||
| 632 | + } | ||
| 633 | + --wanted_last; | ||
| 634 | + } | ||
| 635 | + highlight = true; | ||
| 636 | + highlight_idx = found_idx - wanted_first; | ||
| 637 | + for (int i = wanted_first; i <= wanted_last; ++i) | ||
| 638 | + { | ||
| 639 | + lines.push_back(opt.at(i)); | ||
| 640 | + } | ||
| 641 | + } | ||
| 642 | + else | ||
| 643 | + { | ||
| 644 | + QTC::TC("qpdf", "QPDFFormFieldObjectHelper list not found"); | ||
| 645 | + // include our value and the first n-1 rows | ||
| 646 | + highlight_idx = 0; | ||
| 647 | + highlight = true; | ||
| 648 | + lines.push_back(V); | ||
| 649 | + for (size_t i = 0; ((i < nopt) && (i < (max_rows - 1))); ++i) | ||
| 650 | + { | ||
| 651 | + lines.push_back(opt.at(i)); | ||
| 652 | + } | ||
| 653 | + } | ||
| 654 | + } | ||
| 655 | + | ||
| 656 | + // Write the lines centered vertically, highlighting if needed | ||
| 657 | + size_t nlines = lines.size(); | ||
| 658 | + double dy = bbox.ury - ((bbox.ury - bbox.lly - (nlines * tfh)) / 2.0); | ||
| 659 | + write(DA + "\nq\n"); | ||
| 660 | + if (highlight) | ||
| 661 | + { | ||
| 662 | + write("q\n0.85 0.85 0.85 rg\n" + | ||
| 663 | + QUtil::int_to_string(bbox.llx) + " " + | ||
| 664 | + QUtil::double_to_string(bbox.lly + dy - | ||
| 665 | + (tfh * (highlight_idx + 1))) + " " + | ||
| 666 | + QUtil::int_to_string(bbox.urx - bbox.llx) + " " + | ||
| 667 | + QUtil::double_to_string(tfh) + | ||
| 668 | + " re f\nQ\n"); | ||
| 669 | + } | ||
| 670 | + dy += 0.2 * tf; | ||
| 671 | + for (size_t i = 0; i < nlines; ++i) | ||
| 672 | + { | ||
| 673 | + dy -= tfh; | ||
| 674 | + write("BT\n" + | ||
| 675 | + QUtil::int_to_string(bbox.llx + dx) + " " + | ||
| 676 | + QUtil::double_to_string(bbox.lly + dy) + " Td\n" + | ||
| 677 | + QPDFObjectHandle::newString(lines.at(i)).unparse() + | ||
| 678 | + " Tj\nET\n"); | ||
| 679 | + } | ||
| 680 | + write("Q\nEMC"); | ||
| 681 | +} | ||
| 682 | + | ||
| 683 | +class TfFinder: public QPDFObjectHandle::TokenFilter | ||
| 684 | +{ | ||
| 685 | + public: | ||
| 686 | + TfFinder(); | ||
| 687 | + virtual ~TfFinder() | ||
| 688 | + { | ||
| 689 | + } | ||
| 690 | + virtual void handleToken(QPDFTokenizer::Token const&); | ||
| 691 | + double getTf(); | ||
| 692 | + | ||
| 693 | + private: | ||
| 694 | + double tf; | ||
| 695 | + double last_num; | ||
| 696 | +}; | ||
| 697 | + | ||
| 698 | +TfFinder::TfFinder() : | ||
| 699 | + tf(11.0), | ||
| 700 | + last_num(0.0) | ||
| 701 | +{ | ||
| 702 | +} | ||
| 703 | + | ||
| 704 | +void | ||
| 705 | +TfFinder::handleToken(QPDFTokenizer::Token const& token) | ||
| 706 | +{ | ||
| 707 | + QPDFTokenizer::token_type_e ttype = token.getType(); | ||
| 708 | + std::string value = token.getValue(); | ||
| 709 | + switch (ttype) | ||
| 710 | + { | ||
| 711 | + case QPDFTokenizer::tt_integer: | ||
| 712 | + case QPDFTokenizer::tt_real: | ||
| 713 | + last_num = strtod(value.c_str(), 0); | ||
| 714 | + break; | ||
| 715 | + | ||
| 716 | + case QPDFTokenizer::tt_word: | ||
| 717 | + if ((value == "Tf") && | ||
| 718 | + (last_num > 1.0) && | ||
| 719 | + (last_num < 1000.0)) | ||
| 720 | + { | ||
| 721 | + // These ranges are arbitrary but keep us from doing | ||
| 722 | + // insane things or suffering from over/underflow | ||
| 723 | + tf = last_num; | ||
| 724 | + } | ||
| 725 | + break; | ||
| 726 | + | ||
| 727 | + default: | ||
| 728 | + break; | ||
| 729 | + } | ||
| 730 | +} | ||
| 731 | + | ||
| 732 | +double | ||
| 733 | +TfFinder::getTf() | ||
| 734 | +{ | ||
| 735 | + return this->tf; | ||
| 736 | +} | ||
| 737 | + | ||
| 738 | +void | ||
| 739 | +QPDFFormFieldObjectHelper::generateTextAppearance( | ||
| 740 | + QPDFAnnotationObjectHelper& aoh) | ||
| 741 | +{ | ||
| 742 | + QPDFObjectHandle AS = aoh.getAppearanceStream("/N"); | ||
| 743 | + if (! AS.isStream()) | ||
| 744 | + { | ||
| 745 | + aoh.getObjectHandle().warnIfPossible( | ||
| 746 | + "unable to get normal appearance stream for update"); | ||
| 747 | + return; | ||
| 748 | + } | ||
| 749 | + QPDFObjectHandle bbox_obj = AS.getDict().getKey("/BBox"); | ||
| 750 | + if (! bbox_obj.isRectangle()) | ||
| 751 | + { | ||
| 752 | + aoh.getObjectHandle().warnIfPossible( | ||
| 753 | + "unable to get appearance stream bounding box"); | ||
| 754 | + return; | ||
| 755 | + } | ||
| 756 | + QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle(); | ||
| 757 | + std::string DA = getDefaultAppearance(); | ||
| 758 | + std::string V = QUtil::utf8_to_ascii(getValueAsString()); | ||
| 759 | + | ||
| 760 | + TfFinder tff; | ||
| 761 | + Pl_QPDFTokenizer tok("tf", &tff); | ||
| 762 | + tok.write(QUtil::unsigned_char_pointer(DA.c_str()), DA.length()); | ||
| 763 | + tok.finish(); | ||
| 764 | + double tf = tff.getTf(); | ||
| 765 | + std::vector<std::string> opt; | ||
| 766 | + if (isChoice() && ((getFlags() & ff_ch_combo) == 0)) | ||
| 767 | + { | ||
| 768 | + opt = getChoices(); | ||
| 769 | + } | ||
| 770 | + AS.addTokenFilter(new ValueSetter(DA, V, opt, tf, bbox)); | ||
| 771 | +} |
qpdf/qpdf.cc
| @@ -106,6 +106,7 @@ struct Options | @@ -106,6 +106,7 @@ struct Options | ||
| 106 | flatten_annotations(false), | 106 | flatten_annotations(false), |
| 107 | flatten_annotations_required(0), | 107 | flatten_annotations_required(0), |
| 108 | flatten_annotations_forbidden(an_invisible | an_hidden), | 108 | flatten_annotations_forbidden(an_invisible | an_hidden), |
| 109 | + generate_appearances(false), | ||
| 109 | show_npages(false), | 110 | show_npages(false), |
| 110 | deterministic_id(false), | 111 | deterministic_id(false), |
| 111 | static_id(false), | 112 | static_id(false), |
| @@ -181,6 +182,7 @@ struct Options | @@ -181,6 +182,7 @@ struct Options | ||
| 181 | bool flatten_annotations; | 182 | bool flatten_annotations; |
| 182 | int flatten_annotations_required; | 183 | int flatten_annotations_required; |
| 183 | int flatten_annotations_forbidden; | 184 | int flatten_annotations_forbidden; |
| 185 | + bool generate_appearances; | ||
| 184 | std::string min_version; | 186 | std::string min_version; |
| 185 | std::string force_version; | 187 | std::string force_version; |
| 186 | bool show_npages; | 188 | bool show_npages; |
| @@ -570,6 +572,7 @@ class ArgParser | @@ -570,6 +572,7 @@ class ArgParser | ||
| 570 | void argLinearizePass1(char* parameter); | 572 | void argLinearizePass1(char* parameter); |
| 571 | void argCoalesceContents(); | 573 | void argCoalesceContents(); |
| 572 | void argFlattenAnnotations(char* parameter); | 574 | void argFlattenAnnotations(char* parameter); |
| 575 | + void argGenerateAppearances(); | ||
| 573 | void argMinVersion(char* parameter); | 576 | void argMinVersion(char* parameter); |
| 574 | void argForceVersion(char* parameter); | 577 | void argForceVersion(char* parameter); |
| 575 | void argSplitPages(char* parameter); | 578 | void argSplitPages(char* parameter); |
| @@ -771,6 +774,8 @@ ArgParser::initOptionTable() | @@ -771,6 +774,8 @@ ArgParser::initOptionTable() | ||
| 771 | char const* flatten_choices[] = {"all", "print", "screen", 0}; | 774 | char const* flatten_choices[] = {"all", "print", "screen", 0}; |
| 772 | (*t)["flatten-annotations"] = oe_requiredChoices( | 775 | (*t)["flatten-annotations"] = oe_requiredChoices( |
| 773 | &ArgParser::argFlattenAnnotations, flatten_choices); | 776 | &ArgParser::argFlattenAnnotations, flatten_choices); |
| 777 | + (*t)["generate-appearances"] = | ||
| 778 | + oe_bare(&ArgParser::argGenerateAppearances); | ||
| 774 | (*t)["min-version"] = oe_requiredParameter( | 779 | (*t)["min-version"] = oe_requiredParameter( |
| 775 | &ArgParser::argMinVersion, "version"); | 780 | &ArgParser::argMinVersion, "version"); |
| 776 | (*t)["force-version"] = oe_requiredParameter( | 781 | (*t)["force-version"] = oe_requiredParameter( |
| @@ -1234,6 +1239,12 @@ ArgParser::argFlattenAnnotations(char* parameter) | @@ -1234,6 +1239,12 @@ ArgParser::argFlattenAnnotations(char* parameter) | ||
| 1234 | } | 1239 | } |
| 1235 | 1240 | ||
| 1236 | void | 1241 | void |
| 1242 | +ArgParser::argGenerateAppearances() | ||
| 1243 | +{ | ||
| 1244 | + o.generate_appearances = true; | ||
| 1245 | +} | ||
| 1246 | + | ||
| 1247 | +void | ||
| 1237 | ArgParser::argMinVersion(char* parameter) | 1248 | ArgParser::argMinVersion(char* parameter) |
| 1238 | { | 1249 | { |
| 1239 | o.min_version = parameter; | 1250 | o.min_version = parameter; |
| @@ -1877,18 +1888,28 @@ familiar with the PDF file format or who are PDF developers.\n\ | @@ -1877,18 +1888,28 @@ familiar with the PDF file format or who are PDF developers.\n\ | ||
| 1877 | --flatten-annotations=option\n\ | 1888 | --flatten-annotations=option\n\ |
| 1878 | incorporate rendering of annotations into page\n\ | 1889 | incorporate rendering of annotations into page\n\ |
| 1879 | contents including those for interactive form\n\ | 1890 | contents including those for interactive form\n\ |
| 1880 | - fields\n\ | 1891 | + fields; may also want --generate-appearances\n\ |
| 1892 | +--generate-appearances generate appearance streams for form fields\n\ | ||
| 1881 | --qdf turns on \"QDF mode\" (below)\n\ | 1893 | --qdf turns on \"QDF mode\" (below)\n\ |
| 1882 | --linearize-pass1=file write intermediate pass of linearized file\n\ | 1894 | --linearize-pass1=file write intermediate pass of linearized file\n\ |
| 1883 | for debugging\n\ | 1895 | for debugging\n\ |
| 1884 | --min-version=version sets the minimum PDF version of the output file\n\ | 1896 | --min-version=version sets the minimum PDF version of the output file\n\ |
| 1885 | --force-version=version forces this to be the PDF version of the output file\n\ | 1897 | --force-version=version forces this to be the PDF version of the output file\n\ |
| 1886 | \n\ | 1898 | \n\ |
| 1887 | -Options for --flatten-annotations are all, print, or screen. If the\n\ | ||
| 1888 | -option is print, only annotations marked as print are included. If the\n\ | ||
| 1889 | -option is screen, options marked as \"no view\" are excluded.\n\ | ||
| 1890 | -Otherwise, annotations are flattened regardless of the presence of\n\ | ||
| 1891 | -print or NoView flags.\n\ | 1899 | +Options for --flatten-annotations are all, print, or screen. If the option\n\ |
| 1900 | +is print, only annotations marked as print are included. If the option is\n\ | ||
| 1901 | +screen, options marked as \"no view\" are excluded. Otherwise, annotations\n\ | ||
| 1902 | +are flattened regardless of the presence of print or NoView flags. It is\n\ | ||
| 1903 | +common for PDF files to have a flag set that appearance streams need to be\n\ | ||
| 1904 | +regenerated. This happens when someone changes a form value with software\n\ | ||
| 1905 | +that does not know how to render the new value. qpdf will not flatten form\n\ | ||
| 1906 | +fields in files like this. If you get this warning, you have two choices:\n\ | ||
| 1907 | +either use qpdf's --generate-appearances flag to tell qpdf to go ahead and\n\ | ||
| 1908 | +regenerate appearances, or use some other tool to generate the appearances.\n\ | ||
| 1909 | +qpdf does a pretty good job with most forms when only ASCII characters are\n\ | ||
| 1910 | +used in form field values, but if your form fields contain other\n\ | ||
| 1911 | +characters, rich text, or are other than left justified, you will get\n\ | ||
| 1912 | +better results first saving with other software.\n\ | ||
| 1892 | \n\ | 1913 | \n\ |
| 1893 | Version numbers may be expressed as major.minor.extension-level, so 1.7.3\n\ | 1914 | Version numbers may be expressed as major.minor.extension-level, so 1.7.3\n\ |
| 1894 | means PDF version 1.7 at extension level 3.\n\ | 1915 | means PDF version 1.7 at extension level 3.\n\ |
| @@ -3356,6 +3377,11 @@ static void do_inspection(QPDF& pdf, Options& o) | @@ -3356,6 +3377,11 @@ static void do_inspection(QPDF& pdf, Options& o) | ||
| 3356 | static void handle_transformations(QPDF& pdf, Options& o) | 3377 | static void handle_transformations(QPDF& pdf, Options& o) |
| 3357 | { | 3378 | { |
| 3358 | QPDFPageDocumentHelper dh(pdf); | 3379 | QPDFPageDocumentHelper dh(pdf); |
| 3380 | + if (o.generate_appearances) | ||
| 3381 | + { | ||
| 3382 | + QPDFAcroFormDocumentHelper afdh(pdf); | ||
| 3383 | + afdh.generateAppearancesIfNeeded(); | ||
| 3384 | + } | ||
| 3359 | if (o.flatten_annotations) | 3385 | if (o.flatten_annotations) |
| 3360 | { | 3386 | { |
| 3361 | dh.flattenAnnotations(o.flatten_annotations_required, | 3387 | dh.flattenAnnotations(o.flatten_annotations_required, |
qpdf/qpdf.testcov
| @@ -398,3 +398,7 @@ QPDFFormFieldObjectHelper checkbox kid widget 0 | @@ -398,3 +398,7 @@ QPDFFormFieldObjectHelper checkbox kid widget 0 | ||
| 398 | QPDFObjectHandle broken radio button 0 | 398 | QPDFObjectHandle broken radio button 0 |
| 399 | QPDFFormFieldObjectHelper set checkbox AS 0 | 399 | QPDFFormFieldObjectHelper set checkbox AS 0 |
| 400 | QPDFObjectHandle broken checkbox 0 | 400 | QPDFObjectHandle broken checkbox 0 |
| 401 | +QPDFFormFieldObjectHelper list not found 0 | ||
| 402 | +QPDFFormFieldObjectHelper list found 0 | ||
| 403 | +QPDFFormFieldObjectHelper list first too low 0 | ||
| 404 | +QPDFFormFieldObjectHelper list last too high 0 |
qpdf/qtest/qpdf.test
| @@ -234,6 +234,57 @@ $td->runtest("compare files", | @@ -234,6 +234,57 @@ $td->runtest("compare files", | ||
| 234 | 234 | ||
| 235 | show_ntests(); | 235 | show_ntests(); |
| 236 | # ---------- | 236 | # ---------- |
| 237 | +$td->notify("--- Appearance Streams ---"); | ||
| 238 | +$n_tests += 4; | ||
| 239 | + | ||
| 240 | +$td->runtest("generate appearances and flatten", | ||
| 241 | + {$td->COMMAND => | ||
| 242 | + "qpdf --qdf --no-original-object-ids --static-id" . | ||
| 243 | + " --generate-appearances --flatten-annotations=all" . | ||
| 244 | + " need-appearances.pdf a.pdf"}, | ||
| 245 | + {$td->STRING => "", $td->EXIT_STATUS => 0}, | ||
| 246 | + $td->NORMALIZE_NEWLINES); | ||
| 247 | +$td->runtest("compare files", | ||
| 248 | + {$td->FILE => "a.pdf"}, | ||
| 249 | + {$td->FILE => "appearances-a.pdf"}); | ||
| 250 | + | ||
| 251 | +$td->runtest("more choices", | ||
| 252 | + {$td->COMMAND => | ||
| 253 | + "qpdf --qdf --no-original-object-ids --static-id" . | ||
| 254 | + " --generate-appearances" . | ||
| 255 | + " more-choices.pdf b.pdf"}, | ||
| 256 | + {$td->STRING => "", $td->EXIT_STATUS => 0}, | ||
| 257 | + $td->NORMALIZE_NEWLINES); | ||
| 258 | +# b.pdf still has forms | ||
| 259 | +$td->runtest("compare files", | ||
| 260 | + {$td->FILE => "b.pdf"}, | ||
| 261 | + {$td->FILE => "appearances-b.pdf"}); | ||
| 262 | + | ||
| 263 | +my @choice_values = qw(1 2 11 12 quack); | ||
| 264 | +$n_tests += 3 * scalar(@choice_values); | ||
| 265 | +foreach my $i (@choice_values) | ||
| 266 | +{ | ||
| 267 | + # b.pdf was generated by qpdf and needs appearances | ||
| 268 | + # test_driver 52 writes a.pdf | ||
| 269 | + $td->runtest("set value to $i", | ||
| 270 | + {$td->COMMAND => "test_driver 52 b.pdf $i"}, | ||
| 271 | + {$td->STRING => "setting list1 value\ntest 52 done\n", | ||
| 272 | + $td->EXIT_STATUS => 0}, | ||
| 273 | + $td->NORMALIZE_NEWLINES); | ||
| 274 | + $td->runtest("regenerate appearances", | ||
| 275 | + {$td->COMMAND => | ||
| 276 | + "qpdf --qdf --no-original-object-ids --static-id" . | ||
| 277 | + " --generate-appearances" . | ||
| 278 | + " a.pdf b.pdf"}, | ||
| 279 | + {$td->STRING => "", $td->EXIT_STATUS => 0}, | ||
| 280 | + $td->NORMALIZE_NEWLINES); | ||
| 281 | + $td->runtest("compare files", | ||
| 282 | + {$td->FILE => "b.pdf"}, | ||
| 283 | + {$td->FILE => "appearances-$i.pdf"}); | ||
| 284 | +} | ||
| 285 | + | ||
| 286 | +show_ntests(); | ||
| 287 | +# ---------- | ||
| 237 | $td->notify("--- Stream Replacement Tests ---"); | 288 | $td->notify("--- Stream Replacement Tests ---"); |
| 238 | $n_tests += 8; | 289 | $n_tests += 8; |
| 239 | 290 |
qpdf/qtest/qpdf/appearances-1.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/appearances-11.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/appearances-12.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/appearances-2.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/appearances-a.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/appearances-b.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/appearances-quack.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/json-need-appearances-acroform.out
| @@ -321,7 +321,7 @@ | @@ -321,7 +321,7 @@ | ||
| 321 | "parent": null, | 321 | "parent": null, |
| 322 | "partialname": "list1", | 322 | "partialname": "list1", |
| 323 | "quadding": 0, | 323 | "quadding": 0, |
| 324 | - "value": "five" | 324 | + "value": "six" |
| 325 | }, | 325 | }, |
| 326 | { | 326 | { |
| 327 | "alternativename": "drop1", | 327 | "alternativename": "drop1", |
qpdf/qtest/qpdf/more-choices.pdf
0 → 100644
No preview for this file type
qpdf/qtest/qpdf/need-appearances-out.pdf
No preview for this file type
qpdf/qtest/qpdf/need-appearances.pdf
No preview for this file type
qpdf/test_driver.cc
| @@ -1820,6 +1820,32 @@ void runtest(int n, char const* filename1, char const* arg2) | @@ -1820,6 +1820,32 @@ void runtest(int n, char const* filename1, char const* arg2) | ||
| 1820 | w.setStaticID(true); | 1820 | w.setStaticID(true); |
| 1821 | w.write(); | 1821 | w.write(); |
| 1822 | } | 1822 | } |
| 1823 | + else if (n == 52) | ||
| 1824 | + { | ||
| 1825 | + // This test just sets a field value for appearance stream | ||
| 1826 | + // generating testing. | ||
| 1827 | + QPDFObjectHandle acroform = pdf.getRoot().getKey("/AcroForm"); | ||
| 1828 | + QPDFObjectHandle fields = acroform.getKey("/Fields"); | ||
| 1829 | + int n = fields.getArrayNItems(); | ||
| 1830 | + for (int i = 0; i < n; ++i) | ||
| 1831 | + { | ||
| 1832 | + QPDFObjectHandle field = fields.getArrayItem(i); | ||
| 1833 | + QPDFObjectHandle T = field.getKey("/T"); | ||
| 1834 | + if (! T.isString()) | ||
| 1835 | + { | ||
| 1836 | + continue; | ||
| 1837 | + } | ||
| 1838 | + std::string Tval = T.getUTF8Value(); | ||
| 1839 | + if (Tval == "list1") | ||
| 1840 | + { | ||
| 1841 | + std::cout << "setting list1 value\n"; | ||
| 1842 | + QPDFFormFieldObjectHelper foh(field); | ||
| 1843 | + foh.setV(QPDFObjectHandle::newString(arg2)); | ||
| 1844 | + } | ||
| 1845 | + } | ||
| 1846 | + QPDFWriter w(pdf, "a.pdf"); | ||
| 1847 | + w.write(); | ||
| 1848 | + } | ||
| 1823 | else | 1849 | else |
| 1824 | { | 1850 | { |
| 1825 | throw std::runtime_error(std::string("invalid test ") + | 1851 | throw std::runtime_error(std::string("invalid test ") + |