Commit fa2516df712aa59eb414933a912d30bb6fa1606e

Authored by Jay Berkenbilt
1 parent 5207c3da

Fix behavior for finding /Q, /DA, and /DR for form fields

If not found in the field hierarchy, /Q and /DA are supposed to be
looked up in the document-level form dictionary. /DR is supposed to
only come from the document dictionary.
ChangeLog
  1 +2021-02-26 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Bug fix: QPDFFormFieldObjectHelper was mis-handling /DA, /Q, and
  4 + /DR in ways that usually didn't matter but were still wrong. /DA
  5 + and /Q were being found in the field hierarchy, but if not found,
  6 + the default values in the /AcroForm dictionary were not being
  7 + used. /DR was being treated as an inherited field in the field
  8 + dictionary, which is wrong. It is actually supposed to come from
  9 + the /AcroForm dictionary. We were getting away with this since
  10 + many popular form writers seem to copy it to the field as well,
  11 + even though the spec makes no mention of doing this. To support
  12 + this, QPDFFormFieldObjectHelper::getDefaultResources was added.
  13 +
1 2021-02-25 Jay Berkenbilt <ejb@ql.org> 14 2021-02-25 Jay Berkenbilt <ejb@ql.org>
2 15
3 * Update StreamDataProvider examples to use copyStream() when they 16 * Update StreamDataProvider examples to use copyStream() when they
include/qpdf/QPDFFormFieldObjectHelper.hh
@@ -121,12 +121,22 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper @@ -121,12 +121,22 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper
121 // the field tree into account. Returns the empty string if the 121 // the field tree into account. Returns the empty string if the
122 // default appearance string is not available (because it's 122 // default appearance string is not available (because it's
123 // erroneously absent or because this is not a variable text 123 // erroneously absent or because this is not a variable text
124 - // field). 124 + // field). If not found in the field hierarchy, look in /AcroForm.
125 QPDF_DLL 125 QPDF_DLL
126 std::string getDefaultAppearance(); 126 std::string getDefaultAppearance();
127 127
  128 + // Return the default resource dictionary for the field. This
  129 + // comes not from the field but from the document-level /AcroForm
  130 + // dictionary. While several PDF generates put a /DR key in the
  131 + // form field's dictionary, experimentation suggests that many
  132 + // popular readers, including Adobe Acrobat and Acrobat Reader,
  133 + // ignore any /DR item on the field.
  134 + QPDF_DLL
  135 + QPDFObjectHandle getDefaultResources();
  136 +
128 // Return the quadding value, taking inheritance from the field 137 // Return the quadding value, taking inheritance from the field
129 - // tree into account. Returns 0 if quadding is not specified. 138 + // tree into account. Returns 0 if quadding is not specified. Look
  139 + // in /AcroForm if not found in the field hierarchy.
130 QPDF_DLL 140 QPDF_DLL
131 int getQuadding(); 141 int getQuadding();
132 142
@@ -200,6 +210,7 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper @@ -200,6 +210,7 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper
200 void generateAppearance(QPDFAnnotationObjectHelper&); 210 void generateAppearance(QPDFAnnotationObjectHelper&);
201 211
202 private: 212 private:
  213 + QPDFObjectHandle getFieldFromAcroForm(std::string const& name);
203 void setRadioButtonValue(QPDFObjectHandle name); 214 void setRadioButtonValue(QPDFObjectHandle name);
204 void setCheckBoxValue(bool value); 215 void setCheckBoxValue(bool value);
205 void generateTextAppearance(QPDFAnnotationObjectHelper&); 216 void generateTextAppearance(QPDFAnnotationObjectHelper&);
libqpdf/QPDFFormFieldObjectHelper.cc
@@ -63,6 +63,24 @@ QPDFFormFieldObjectHelper::getTopLevelField(bool* is_different) @@ -63,6 +63,24 @@ QPDFFormFieldObjectHelper::getTopLevelField(bool* is_different)
63 } 63 }
64 64
65 QPDFObjectHandle 65 QPDFObjectHandle
  66 +QPDFFormFieldObjectHelper::getFieldFromAcroForm(std::string const& name)
  67 +{
  68 + QPDFObjectHandle result = QPDFObjectHandle::newNull();
  69 + // Fields are supposed to be indirect, so this should work.
  70 + QPDF* q = this->oh.getOwningQPDF();
  71 + if (! q)
  72 + {
  73 + return result;
  74 + }
  75 + auto acroform = q->getRoot().getKey("/AcroForm");
  76 + if (! acroform.isDictionary())
  77 + {
  78 + return result;
  79 + }
  80 + return acroform.getKey(name);
  81 +}
  82 +
  83 +QPDFObjectHandle
66 QPDFFormFieldObjectHelper::getInheritableFieldValue(std::string const& name) 84 QPDFFormFieldObjectHelper::getInheritableFieldValue(std::string const& name)
67 { 85 {
68 QPDFObjectHandle node = this->oh; 86 QPDFObjectHandle node = this->oh;
@@ -203,20 +221,47 @@ QPDFFormFieldObjectHelper::getDefaultValueAsString() @@ -203,20 +221,47 @@ QPDFFormFieldObjectHelper::getDefaultValueAsString()
203 return getInheritableFieldValueAsString("/DV"); 221 return getInheritableFieldValueAsString("/DV");
204 } 222 }
205 223
  224 +QPDFObjectHandle
  225 +QPDFFormFieldObjectHelper::getDefaultResources()
  226 +{
  227 + return getFieldFromAcroForm("/DR");
  228 +}
  229 +
206 std::string 230 std::string
207 QPDFFormFieldObjectHelper::getDefaultAppearance() 231 QPDFFormFieldObjectHelper::getDefaultAppearance()
208 { 232 {
209 - return getInheritableFieldValueAsString("/DA"); 233 + auto value = getInheritableFieldValue("/DA");
  234 + bool looked_in_acroform = false;
  235 + if (! value.isString())
  236 + {
  237 + value = getFieldFromAcroForm("/DA");
  238 + looked_in_acroform = true;
  239 + }
  240 + std::string result;
  241 + if (value.isString())
  242 + {
  243 + QTC::TC("qpdf", "QPDFFormFieldObjectHelper DA present",
  244 + looked_in_acroform ? 0 : 1);
  245 + result = value.getUTF8Value();
  246 + }
  247 + return result;
210 } 248 }
211 249
212 int 250 int
213 QPDFFormFieldObjectHelper::getQuadding() 251 QPDFFormFieldObjectHelper::getQuadding()
214 { 252 {
215 - int result = 0;  
216 QPDFObjectHandle fv = getInheritableFieldValue("/Q"); 253 QPDFObjectHandle fv = getInheritableFieldValue("/Q");
  254 + bool looked_in_acroform = false;
  255 + if (! fv.isInteger())
  256 + {
  257 + fv = getFieldFromAcroForm("/Q");
  258 + looked_in_acroform = true;
  259 + }
  260 + int result = 0;
217 if (fv.isInteger()) 261 if (fv.isInteger())
218 { 262 {
219 - QTC::TC("qpdf", "QPDFFormFieldObjectHelper Q present"); 263 + QTC::TC("qpdf", "QPDFFormFieldObjectHelper Q present",
  264 + looked_in_acroform ? 0 : 1);
220 result = QIntC::to_int(fv.getIntValue()); 265 result = QIntC::to_int(fv.getIntValue());
221 } 266 }
222 return result; 267 return result;
@@ -920,7 +965,7 @@ QPDFFormFieldObjectHelper::generateTextAppearance( @@ -920,7 +965,7 @@ QPDFFormFieldObjectHelper::generateTextAppearance(
920 QPDFObjectHandle font = getFontFromResource(resources, font_name); 965 QPDFObjectHandle font = getFontFromResource(resources, font_name);
921 if (! font.isInitialized()) 966 if (! font.isInitialized())
922 { 967 {
923 - QPDFObjectHandle dr = getInheritableFieldValue("/DR"); 968 + QPDFObjectHandle dr = getDefaultResources();
924 font = getFontFromResource(dr, font_name); 969 font = getFontFromResource(dr, font_name);
925 } 970 }
926 if (font.isInitialized() && 971 if (font.isInitialized() &&
libqpdf/QPDFPageDocumentHelper.cc
@@ -148,8 +148,7 @@ QPDFPageDocumentHelper::flattenAnnotationsForPage( @@ -148,8 +148,7 @@ QPDFPageDocumentHelper::flattenAnnotationsForPage(
148 "/Resources", as_resources.shallowCopy()); 148 "/Resources", as_resources.shallowCopy());
149 as_resources = as.getDict().getKey("/Resources"); 149 as_resources = as.getDict().getKey("/Resources");
150 } 150 }
151 - as_resources.mergeResources(  
152 - ff.getInheritableFieldValue("/DR")); 151 + as_resources.mergeResources(ff.getDefaultResources());
153 } 152 }
154 else 153 else
155 { 154 {
manual/qpdf-manual.xml
@@ -5061,7 +5061,7 @@ print &quot;\n&quot;; @@ -5061,7 +5061,7 @@ print &quot;\n&quot;;
5061 </varlistentry> 5061 </varlistentry>
5062 --> 5062 -->
5063 <varlistentry> 5063 <varlistentry>
5064 - <term>XXX 10.2.1: Month dd, YYYY</term> 5064 + <term>XXX 10.3.0: Month dd, YYYY</term>
5065 <listitem> 5065 <listitem>
5066 <itemizedlist> 5066 <itemizedlist>
5067 <listitem> 5067 <listitem>
@@ -5081,6 +5081,16 @@ print &quot;\n&quot;; @@ -5081,6 +5081,16 @@ print &quot;\n&quot;;
5081 rarely used method call. 5081 rarely used method call.
5082 </para> 5082 </para>
5083 </listitem> 5083 </listitem>
  5084 + <listitem>
  5085 + <para>
  5086 + Fix form field handling code to look for default
  5087 + appearances, quadding, and default resources in the right
  5088 + places. The code was not looking for things in the
  5089 + document-level interactive form dictionary that it was
  5090 + supposed to be finding there. This required adding a few new
  5091 + methods to <classname>QPDFFormFieldObjectHelper</classname>.
  5092 + </para>
  5093 + </listitem>
5084 </itemizedlist> 5094 </itemizedlist>
5085 </listitem> 5095 </listitem>
5086 </itemizedlist> 5096 </itemizedlist>
qpdf/qpdf.testcov
@@ -337,7 +337,8 @@ QPDFFormFieldObjectHelper TU present 0 @@ -337,7 +337,8 @@ QPDFFormFieldObjectHelper TU present 0
337 QPDFFormFieldObjectHelper TM present 0 337 QPDFFormFieldObjectHelper TM present 0
338 QPDFFormFieldObjectHelper TU absent 0 338 QPDFFormFieldObjectHelper TU absent 0
339 QPDFFormFieldObjectHelper TM absent 0 339 QPDFFormFieldObjectHelper TM absent 0
340 -QPDFFormFieldObjectHelper Q present 0 340 +QPDFFormFieldObjectHelper Q present 1
  341 +QPDFFormFieldObjectHelper DA present 1
341 QPDFAnnotationObjectHelper AS present 0 342 QPDFAnnotationObjectHelper AS present 0
342 QPDFAnnotationObjectHelper AS absent 0 343 QPDFAnnotationObjectHelper AS absent 0
343 QPDFAnnotationObjectHelper AP stream 0 344 QPDFAnnotationObjectHelper AP stream 0
qpdf/qtest/qpdf.test
@@ -335,6 +335,7 @@ my @form_tests = ( @@ -335,6 +335,7 @@ my @form_tests = (
335 'form-filled-with-atril', 335 'form-filled-with-atril',
336 'form-bad-fields-array', 336 'form-bad-fields-array',
337 'form-errors', 337 'form-errors',
  338 + 'form-document-defaults',
338 ); 339 );
339 340
340 $n_tests += scalar(@form_tests) + 6; 341 $n_tests += scalar(@form_tests) + 6;
qpdf/qtest/qpdf/form-document-defaults.pdf 0 โ†’ 100644
No preview for this file type
qpdf/qtest/qpdf/form-filled-by-acrobat-out.pdf
No preview for this file type
qpdf/qtest/qpdf/form-form-document-defaults.out 0 โ†’ 100644
  1 +iterating over form fields
  2 +Field: 4 0 R
  3 + Parent: none
  4 + Fully qualified name: Text Box 1
  5 + Partial name: Text Box 1
  6 + Alternative name: Text Box 1
  7 + Mapping name: Text Box 1
  8 + Field type: /Tx
  9 + Value: <feff>
  10 + Value as string:
  11 + Default value: <feff>
  12 + Default value as string:
  13 + Default appearance: 0.18039 0.20392 0.21176 rg /F2 12 Tf
  14 + Quadding: 1
  15 + Annotation: 83 0 R
  16 +Field: 6 0 R
  17 + Parent: none
  18 + Fully qualified name: Check Box 1
  19 + Partial name: Check Box 1
  20 + Alternative name: Check Box 1
  21 + Mapping name: Check Box 1
  22 + Field type: /Btn
  23 + Value: /Off
  24 + Value as string:
  25 + Default value: /Off
  26 + Default value as string:
  27 + Default appearance: 0.18039 0.20392 0.21176 rg /ZaDi 0 Tf
  28 + Quadding: 1
  29 + Annotation: 6 0 R
  30 +Field: 7 0 R
  31 + Parent: none
  32 + Fully qualified name: Check Box 2
  33 + Partial name: Check Box 2
  34 + Alternative name: Check Box 2
  35 + Mapping name: Check Box 2
  36 + Field type: /Btn
  37 + Value: /Yes
  38 + Value as string:
  39 + Default value: /Yes
  40 + Default value as string:
  41 + Default appearance: 0.18039 0.20392 0.21176 rg /ZaDi 0 Tf
  42 + Quadding: 1
  43 + Annotation: 7 0 R
  44 +Field: 8 0 R
  45 + Parent: none
  46 + Fully qualified name: Check Box 3
  47 + Partial name: Check Box 3
  48 + Alternative name: Check Box 3
  49 + Mapping name: Check Box 3
  50 + Field type: /Btn
  51 + Value: /Off
  52 + Value as string:
  53 + Default value: /Off
  54 + Default value as string:
  55 + Default appearance: 0.18039 0.20392 0.21176 rg /ZaDi 0 Tf
  56 + Quadding: 1
  57 + Annotation: 8 0 R
  58 +Field: 10 0 R
  59 + Parent: none
  60 + Fully qualified name: Text Box 2
  61 + Partial name: Text Box 2
  62 + Alternative name: Text Box 2
  63 + Mapping name: Text Box 2
  64 + Field type: /Tx
  65 + Value: <feff00730061006c00610064002003c002ac>
  66 + Value as string: salad ฯ€สฌ
  67 + Default value: <feff00730061006c00610064002003c002ac>
  68 + Default value as string: salad ฯ€สฌ
  69 + Default appearance: 0.18039 0.20392 0.21176 rg /F2 12 Tf
  70 + Quadding: 1
  71 + Annotation: 10 0 R
  72 +Field: 16 0 R
  73 + Parent: 5 0 R
  74 + Parent: none
  75 + Fully qualified name: r1.choice1
  76 + Partial name: choice1
  77 + Alternative name: chice 1
  78 + Mapping name: choice 1 TM
  79 + Field type: /Btn
  80 + Value: /1
  81 + Value as string:
  82 + Default value: /1
  83 + Default value as string:
  84 + Default appearance: 0.18039 0.20392 0.21176 rg /ZaDi 0 Tf
  85 + Quadding: 1
  86 + Annotation: 16 0 R
  87 +Field: 17 0 R
  88 + Parent: 5 0 R
  89 + Parent: none
  90 + Fully qualified name: r1
  91 + Partial name:
  92 + Alternative name: r1
  93 + Mapping name: r1
  94 + Field type: /Btn
  95 + Value: /1
  96 + Value as string:
  97 + Default value: /1
  98 + Default value as string:
  99 + Default appearance: 0.18039 0.20392 0.21176 rg /ZaDi 0 Tf
  100 + Quadding: 1
  101 + Annotation: 17 0 R
  102 +Field: 18 0 R
  103 + Parent: 5 0 R
  104 + Parent: none
  105 + Fully qualified name: r1
  106 + Partial name:
  107 + Alternative name: r1
  108 + Mapping name: r1
  109 + Field type: /Btn
  110 + Value: /1
  111 + Value as string:
  112 + Default value: /1
  113 + Default value as string:
  114 + Default appearance: 0.18039 0.20392 0.21176 rg /ZaDi 0 Tf
  115 + Quadding: 1
  116 + Annotation: 18 0 R
  117 +Field: 32 0 R
  118 + Parent: 9 0 R
  119 + Parent: none
  120 + Fully qualified name: r2
  121 + Partial name:
  122 + Alternative name: r2
  123 + Mapping name: r2
  124 + Field type: /Btn
  125 + Value: /2
  126 + Value as string:
  127 + Default value: /2
  128 + Default value as string:
  129 + Default appearance: 0.18039 0.20392 0.21176 rg /ZaDi 0 Tf
  130 + Quadding: 1
  131 + Annotation: 32 0 R
  132 +Field: 33 0 R
  133 + Parent: 9 0 R
  134 + Parent: none
  135 + Fully qualified name: r2
  136 + Partial name:
  137 + Alternative name: r2
  138 + Mapping name: r2
  139 + Field type: /Btn
  140 + Value: /2
  141 + Value as string:
  142 + Default value: /2
  143 + Default value as string:
  144 + Default appearance: 0.18039 0.20392 0.21176 rg /ZaDi 0 Tf
  145 + Quadding: 1
  146 + Annotation: 33 0 R
  147 +Field: 34 0 R
  148 + Parent: 9 0 R
  149 + Parent: none
  150 + Fully qualified name: r2
  151 + Partial name:
  152 + Alternative name: r2
  153 + Mapping name: r2
  154 + Field type: /Btn
  155 + Value: /2
  156 + Value as string:
  157 + Default value: /2
  158 + Default value as string:
  159 + Default appearance: 0.18039 0.20392 0.21176 rg /ZaDi 0 Tf
  160 + Quadding: 1
  161 + Annotation: 34 0 R
  162 +iterating over annotations per page
  163 +Page: 11 0 R
  164 + Annotation: 83 0 R
  165 + Field: 4 0 R
  166 + Subtype: /Widget
  167 + Rect: [123.4, 692.1, 260.9, 706.7]
  168 + Appearance stream (/N): 14 0 R
  169 + Appearance stream (/N, /3): null
  170 + Annotation: 16 0 R
  171 + Field: 16 0 R
  172 + Subtype: /Widget
  173 + Rect: [149.3, 648.5, 161.6, 660.4]
  174 + Appearance state: /1
  175 + Appearance stream (/N): 44 0 R
  176 + Appearance stream (/N, /3): null
  177 + Annotation: 17 0 R
  178 + Field: 17 0 R
  179 + Subtype: /Widget
  180 + Rect: [152.7, 627.3, 165, 639.2]
  181 + Appearance state: /Off
  182 + Appearance stream (/N): 50 0 R
  183 + Appearance stream (/N, /3): null
  184 + Annotation: 18 0 R
  185 + Field: 18 0 R
  186 + Subtype: /Widget
  187 + Rect: [151.3, 601.7, 163.6, 613.6]
  188 + Appearance state: /Off
  189 + Appearance stream (/N): 54 0 R
  190 + Appearance stream (/N, /3): 52 0 R
  191 + Annotation: 6 0 R
  192 + Field: 6 0 R
  193 + Subtype: /Widget
  194 + Rect: [121.9, 559.1, 134.2, 571]
  195 + Appearance state: /Off
  196 + Appearance stream (/N): 19 0 R
  197 + Appearance stream (/N, /3): null
  198 + Annotation: 7 0 R
  199 + Field: 7 0 R
  200 + Subtype: /Widget
  201 + Rect: [118.6, 527.7, 130.9, 539.6]
  202 + Appearance state: /Yes
  203 + Appearance stream (/N): 26 0 R
  204 + Appearance stream (/N, /3): null
  205 + Annotation: 8 0 R
  206 + Field: 8 0 R
  207 + Subtype: /Widget
  208 + Rect: [118.6, 500.5, 130.9, 512.4]
  209 + Appearance state: /Off
  210 + Appearance stream (/N): 28 0 R
  211 + Appearance stream (/N, /3): null
  212 +Page: 40 0 R
  213 +Page: 35 0 R
  214 + Annotation: 32 0 R
  215 + Field: 32 0 R
  216 + Subtype: /Widget
  217 + Rect: [118.6, 555.7, 130.9, 567.6]
  218 + Appearance state: /Off
  219 + Appearance stream (/N): 58 0 R
  220 + Appearance stream (/N, /3): null
  221 + Annotation: 33 0 R
  222 + Field: 33 0 R
  223 + Subtype: /Widget
  224 + Rect: [119.3, 514.8, 131.6, 526.7]
  225 + Appearance state: /2
  226 + Appearance stream (/N): 60 0 R
  227 + Appearance stream (/N, /3): null
  228 + Annotation: 34 0 R
  229 + Field: 34 0 R
  230 + Subtype: /Widget
  231 + Rect: [121.3, 472.5, 133.6, 484.4]
  232 + Appearance state: /Off
  233 + Appearance stream (/N): 66 0 R
  234 + Appearance stream (/N, /3): 64 0 R
  235 + Annotation: 10 0 R
  236 + Field: 10 0 R
  237 + Subtype: /Widget
  238 + Rect: [113.6, 378.5, 351.3, 396.3]
  239 + Appearance stream (/N): 36 0 R
  240 + Appearance stream (/N, /3): null
  241 +test 43 done
qpdf/qtest/qpdf/sample-form-out.pdf
No preview for this file type