Commit 158156d5062a5ac335bcfde7893be4671affdc32

Authored by Jay Berkenbilt
1 parent 02281632

Add basic appearance stream generation

ChangeLog
1 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 26 * Add method QUtil::utf8_to_ascii, which returns an ASCII string
4 27 for a UTF-8 string, replacing out-of-range characters with a
5 28 specified substitute.
... ...
include/qpdf/QPDFAcroFormDocumentHelper.hh
... ... @@ -157,6 +157,16 @@ class QPDFAcroFormDocumentHelper: public QPDFDocumentHelper
157 157 QPDF_DLL
158 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 170 private:
161 171 void analyze();
162 172 void traverseField(QPDFObjectHandle field,
... ...
include/qpdf/QPDFFormFieldObjectHelper.hh
... ... @@ -31,6 +31,8 @@
31 31 #include <qpdf/DLL.h>
32 32 #include <vector>
33 33  
  34 +class QPDFAnnotationObjectHelper;
  35 +
34 36 class QPDFFormFieldObjectHelper: public QPDFObjectHelper
35 37 {
36 38 public:
... ... @@ -179,9 +181,21 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper
179 181 QPDF_DLL
180 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 195 private:
183 196 void setRadioButtonValue(QPDFObjectHandle name);
184 197 void setCheckBoxValue(bool value);
  198 + void generateTextAppearance(QPDFAnnotationObjectHelper&);
185 199  
186 200 class Members
187 201 {
... ...
libqpdf/QPDFAcroFormDocumentHelper.cc
... ... @@ -285,3 +285,48 @@ QPDFAcroFormDocumentHelper::setNeedAppearances(bool val)
285 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 1 #include <qpdf/QPDFFormFieldObjectHelper.hh>
2 2 #include <qpdf/QTC.hh>
3 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 9 QPDFFormFieldObjectHelper::Members::~Members()
6 10 {
... ... @@ -313,7 +317,15 @@ QPDFFormFieldObjectHelper::setV(
313 317 }
314 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 329 if (need_appearances)
318 330 {
319 331 QPDF* qpdf = this->oh.getOwningQPDF();
... ... @@ -470,3 +482,290 @@ QPDFFormFieldObjectHelper::setCheckBoxValue(bool value)
470 482 QTC::TC("qpdf", "QPDFFormFieldObjectHelper set checkbox AS");
471 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 106 flatten_annotations(false),
107 107 flatten_annotations_required(0),
108 108 flatten_annotations_forbidden(an_invisible | an_hidden),
  109 + generate_appearances(false),
109 110 show_npages(false),
110 111 deterministic_id(false),
111 112 static_id(false),
... ... @@ -181,6 +182,7 @@ struct Options
181 182 bool flatten_annotations;
182 183 int flatten_annotations_required;
183 184 int flatten_annotations_forbidden;
  185 + bool generate_appearances;
184 186 std::string min_version;
185 187 std::string force_version;
186 188 bool show_npages;
... ... @@ -570,6 +572,7 @@ class ArgParser
570 572 void argLinearizePass1(char* parameter);
571 573 void argCoalesceContents();
572 574 void argFlattenAnnotations(char* parameter);
  575 + void argGenerateAppearances();
573 576 void argMinVersion(char* parameter);
574 577 void argForceVersion(char* parameter);
575 578 void argSplitPages(char* parameter);
... ... @@ -771,6 +774,8 @@ ArgParser::initOptionTable()
771 774 char const* flatten_choices[] = {"all", "print", "screen", 0};
772 775 (*t)["flatten-annotations"] = oe_requiredChoices(
773 776 &ArgParser::argFlattenAnnotations, flatten_choices);
  777 + (*t)["generate-appearances"] =
  778 + oe_bare(&ArgParser::argGenerateAppearances);
774 779 (*t)["min-version"] = oe_requiredParameter(
775 780 &ArgParser::argMinVersion, "version");
776 781 (*t)["force-version"] = oe_requiredParameter(
... ... @@ -1234,6 +1239,12 @@ ArgParser::argFlattenAnnotations(char* parameter)
1234 1239 }
1235 1240  
1236 1241 void
  1242 +ArgParser::argGenerateAppearances()
  1243 +{
  1244 + o.generate_appearances = true;
  1245 +}
  1246 +
  1247 +void
1237 1248 ArgParser::argMinVersion(char* parameter)
1238 1249 {
1239 1250 o.min_version = parameter;
... ... @@ -1877,18 +1888,28 @@ familiar with the PDF file format or who are PDF developers.\n\
1877 1888 --flatten-annotations=option\n\
1878 1889 incorporate rendering of annotations into page\n\
1879 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 1893 --qdf turns on \"QDF mode\" (below)\n\
1882 1894 --linearize-pass1=file write intermediate pass of linearized file\n\
1883 1895 for debugging\n\
1884 1896 --min-version=version sets the minimum PDF version of the output file\n\
1885 1897 --force-version=version forces this to be the PDF version of the output file\n\
1886 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 1913 \n\
1893 1914 Version numbers may be expressed as major.minor.extension-level, so 1.7.3\n\
1894 1915 means PDF version 1.7 at extension level 3.\n\
... ... @@ -3356,6 +3377,11 @@ static void do_inspection(QPDF&amp; pdf, Options&amp; o)
3356 3377 static void handle_transformations(QPDF& pdf, Options& o)
3357 3378 {
3358 3379 QPDFPageDocumentHelper dh(pdf);
  3380 + if (o.generate_appearances)
  3381 + {
  3382 + QPDFAcroFormDocumentHelper afdh(pdf);
  3383 + afdh.generateAppearancesIfNeeded();
  3384 + }
3359 3385 if (o.flatten_annotations)
3360 3386 {
3361 3387 dh.flattenAnnotations(o.flatten_annotations_required,
... ...
qpdf/qpdf.testcov
... ... @@ -398,3 +398,7 @@ QPDFFormFieldObjectHelper checkbox kid widget 0
398 398 QPDFObjectHandle broken radio button 0
399 399 QPDFFormFieldObjectHelper set checkbox AS 0
400 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-&gt;runtest(&quot;compare files&quot;,
234 234  
235 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 288 $td->notify("--- Stream Replacement Tests ---");
238 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 321 "parent": null,
322 322 "partialname": "list1",
323 323 "quadding": 0,
324   - "value": "five"
  324 + "value": "six"
325 325 },
326 326 {
327 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 1820 w.setStaticID(true);
1821 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 1849 else
1824 1850 {
1825 1851 throw std::runtime_error(std::string("invalid test ") +
... ...