Commit fa2516df712aa59eb414933a912d30bb6fa1606e
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.
Showing
11 changed files
with
2837 additions
and
24 deletions
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 | 14 | 2021-02-25 Jay Berkenbilt <ejb@ql.org> |
| 2 | 15 | |
| 3 | 16 | * Update StreamDataProvider examples to use copyStream() when they | ... | ... |
include/qpdf/QPDFFormFieldObjectHelper.hh
| ... | ... | @@ -121,12 +121,22 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper |
| 121 | 121 | // the field tree into account. Returns the empty string if the |
| 122 | 122 | // default appearance string is not available (because it's |
| 123 | 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 | 125 | QPDF_DLL |
| 126 | 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 | 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 | 140 | QPDF_DLL |
| 131 | 141 | int getQuadding(); |
| 132 | 142 | |
| ... | ... | @@ -200,6 +210,7 @@ class QPDFFormFieldObjectHelper: public QPDFObjectHelper |
| 200 | 210 | void generateAppearance(QPDFAnnotationObjectHelper&); |
| 201 | 211 | |
| 202 | 212 | private: |
| 213 | + QPDFObjectHandle getFieldFromAcroForm(std::string const& name); | |
| 203 | 214 | void setRadioButtonValue(QPDFObjectHandle name); |
| 204 | 215 | void setCheckBoxValue(bool value); |
| 205 | 216 | void generateTextAppearance(QPDFAnnotationObjectHelper&); | ... | ... |
libqpdf/QPDFFormFieldObjectHelper.cc
| ... | ... | @@ -63,6 +63,24 @@ QPDFFormFieldObjectHelper::getTopLevelField(bool* is_different) |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | 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 | 84 | QPDFFormFieldObjectHelper::getInheritableFieldValue(std::string const& name) |
| 67 | 85 | { |
| 68 | 86 | QPDFObjectHandle node = this->oh; |
| ... | ... | @@ -203,20 +221,47 @@ QPDFFormFieldObjectHelper::getDefaultValueAsString() |
| 203 | 221 | return getInheritableFieldValueAsString("/DV"); |
| 204 | 222 | } |
| 205 | 223 | |
| 224 | +QPDFObjectHandle | |
| 225 | +QPDFFormFieldObjectHelper::getDefaultResources() | |
| 226 | +{ | |
| 227 | + return getFieldFromAcroForm("/DR"); | |
| 228 | +} | |
| 229 | + | |
| 206 | 230 | std::string |
| 207 | 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 | 250 | int |
| 213 | 251 | QPDFFormFieldObjectHelper::getQuadding() |
| 214 | 252 | { |
| 215 | - int result = 0; | |
| 216 | 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 | 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 | 265 | result = QIntC::to_int(fv.getIntValue()); |
| 221 | 266 | } |
| 222 | 267 | return result; |
| ... | ... | @@ -920,7 +965,7 @@ QPDFFormFieldObjectHelper::generateTextAppearance( |
| 920 | 965 | QPDFObjectHandle font = getFontFromResource(resources, font_name); |
| 921 | 966 | if (! font.isInitialized()) |
| 922 | 967 | { |
| 923 | - QPDFObjectHandle dr = getInheritableFieldValue("/DR"); | |
| 968 | + QPDFObjectHandle dr = getDefaultResources(); | |
| 924 | 969 | font = getFontFromResource(dr, font_name); |
| 925 | 970 | } |
| 926 | 971 | if (font.isInitialized() && | ... | ... |
libqpdf/QPDFPageDocumentHelper.cc
| ... | ... | @@ -148,8 +148,7 @@ QPDFPageDocumentHelper::flattenAnnotationsForPage( |
| 148 | 148 | "/Resources", as_resources.shallowCopy()); |
| 149 | 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 | 153 | else |
| 155 | 154 | { | ... | ... |
manual/qpdf-manual.xml
| ... | ... | @@ -5061,7 +5061,7 @@ print "\n"; |
| 5061 | 5061 | </varlistentry> |
| 5062 | 5062 | --> |
| 5063 | 5063 | <varlistentry> |
| 5064 | - <term>XXX 10.2.1: Month dd, YYYY</term> | |
| 5064 | + <term>XXX 10.3.0: Month dd, YYYY</term> | |
| 5065 | 5065 | <listitem> |
| 5066 | 5066 | <itemizedlist> |
| 5067 | 5067 | <listitem> |
| ... | ... | @@ -5081,6 +5081,16 @@ print "\n"; |
| 5081 | 5081 | rarely used method call. |
| 5082 | 5082 | </para> |
| 5083 | 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 | 5094 | </itemizedlist> |
| 5085 | 5095 | </listitem> |
| 5086 | 5096 | </itemizedlist> | ... | ... |
qpdf/qpdf.testcov
| ... | ... | @@ -337,7 +337,8 @@ QPDFFormFieldObjectHelper TU present 0 |
| 337 | 337 | QPDFFormFieldObjectHelper TM present 0 |
| 338 | 338 | QPDFFormFieldObjectHelper TU absent 0 |
| 339 | 339 | QPDFFormFieldObjectHelper TM absent 0 |
| 340 | -QPDFFormFieldObjectHelper Q present 0 | |
| 340 | +QPDFFormFieldObjectHelper Q present 1 | |
| 341 | +QPDFFormFieldObjectHelper DA present 1 | |
| 341 | 342 | QPDFAnnotationObjectHelper AS present 0 |
| 342 | 343 | QPDFAnnotationObjectHelper AS absent 0 |
| 343 | 344 | QPDFAnnotationObjectHelper AP stream 0 | ... | ... |
qpdf/qtest/qpdf.test
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