Skip to content

Commit 77b754e

Browse files
committed
TIKA-4744 - fix pdf tags
1 parent 92220e0 commit 77b754e

3 files changed

Lines changed: 96 additions & 63 deletions

File tree

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/XFAExtractor.java

Lines changed: 71 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -74,77 +74,85 @@ class XFAExtractor {
7474

7575
void extract(InputStream xfaIs, XHTMLContentHandler xhtml, Metadata m, ParseContext context)
7676
throws XMLStreamException, SAXException {
77+
// The reader loop below can throw XMLStreamException on malformed XFA
78+
// XML; the caller (AbstractPDF2XHTML.extractAcroForm) catches that and
79+
// falls through to the AcroForm path, which emits more content. If the
80+
// xfa_content div is left open at the throw point, that fallback
81+
// content nests under it and the outer </body> can't balance. Wrap the
82+
// body in try/finally so the open is always paired with a close.
7783
xhtml.startElement("div", "class", "xfa_content");
78-
79-
//TODO - replace this with multivalued map? This isn't
80-
//actually metadata, just a handy data structure.
81-
Metadata pdfObjRToValues = new Metadata();
82-
83-
//for now, store and dump the fields in insertion order
84-
Map<String, XFAField> namedFields = new LinkedHashMap<>();
85-
86-
//The strategy is to cache the fields in fields
87-
//and cache the values in pdfObjRToValues while
88-
//handling the text etc along the way.
89-
//
90-
//As a final step, dump the merged fields and the values.
91-
92-
XMLStreamReader reader = XMLReaderUtils.getXMLInputFactory(context).createXMLStreamReader(xfaIs);
93-
while (reader.hasNext()) {
94-
switch (reader.next()) {
95-
case XMLStreamConstants.START_ELEMENT:
96-
QName name = reader.getName();
97-
String localName = name.getLocalPart();
98-
if (xfaTemplateMatcher.reset(name.getNamespaceURI()).find() &&
99-
FIELD_LN.equals(name.getLocalPart())) {
100-
handleField(reader, namedFields);
101-
} else if (XFA_DATA.equals(name)) { //full qname match is important!
102-
loadData(reader, pdfObjRToValues);
103-
} else if (textMatcher.reset(localName).find()) {
104-
scrapeTextUntil(reader, xhtml, name);
105-
}
106-
break;
107-
case XMLStreamConstants.END_ELEMENT:
108-
break;
84+
try {
85+
//TODO - replace this with multivalued map? This isn't
86+
//actually metadata, just a handy data structure.
87+
Metadata pdfObjRToValues = new Metadata();
88+
89+
//for now, store and dump the fields in insertion order
90+
Map<String, XFAField> namedFields = new LinkedHashMap<>();
91+
92+
//The strategy is to cache the fields in fields
93+
//and cache the values in pdfObjRToValues while
94+
//handling the text etc along the way.
95+
//
96+
//As a final step, dump the merged fields and the values.
97+
98+
XMLStreamReader reader =
99+
XMLReaderUtils.getXMLInputFactory(context).createXMLStreamReader(xfaIs);
100+
while (reader.hasNext()) {
101+
switch (reader.next()) {
102+
case XMLStreamConstants.START_ELEMENT:
103+
QName name = reader.getName();
104+
String localName = name.getLocalPart();
105+
if (xfaTemplateMatcher.reset(name.getNamespaceURI()).find() &&
106+
FIELD_LN.equals(name.getLocalPart())) {
107+
handleField(reader, namedFields);
108+
} else if (XFA_DATA.equals(name)) { //full qname match is important!
109+
loadData(reader, pdfObjRToValues);
110+
} else if (textMatcher.reset(localName).find()) {
111+
scrapeTextUntil(reader, xhtml, name);
112+
}
113+
break;
114+
case XMLStreamConstants.END_ELEMENT:
115+
break;
116+
}
109117
}
110-
}
111118

112-
if (namedFields.size() == 0) {
113-
xhtml.endElement("div");
114-
return;
115-
}
116-
//now dump fields and values
117-
xhtml.startElement("div", "class", "xfa_form");
118-
xhtml.startElement("ol");
119-
StringBuilder sb = new StringBuilder();
120-
for (Map.Entry<String, XFAField> e : namedFields.entrySet()) {
121-
String fieldName = e.getKey();
122-
XFAField field = e.getValue();
123-
String displayFieldName =
124-
(field.toolTip == null || field.toolTip.isBlank()) ? fieldName :
125-
field.toolTip;
126-
String[] fieldValues = pdfObjRToValues.getValues(fieldName);
127-
if (fieldValues.length == 0) {
128-
fieldValues = new String[]{""};
119+
if (namedFields.size() == 0) {
120+
return;
129121
}
130-
for (String fieldValue : fieldValues) {
131-
AttributesImpl attrs = new AttributesImpl();
132-
attrs.addAttribute("", "fieldName", "fieldName", "CDATA", fieldName);
133-
134-
sb.append(displayFieldName).append(": ");
135-
if (fieldValue != null) {
136-
sb.append(fieldValue);
122+
//now dump fields and values
123+
xhtml.startElement("div", "class", "xfa_form");
124+
xhtml.startElement("ol");
125+
StringBuilder sb = new StringBuilder();
126+
for (Map.Entry<String, XFAField> e : namedFields.entrySet()) {
127+
String fieldName = e.getKey();
128+
XFAField field = e.getValue();
129+
String displayFieldName =
130+
(field.toolTip == null || field.toolTip.isBlank()) ? fieldName :
131+
field.toolTip;
132+
String[] fieldValues = pdfObjRToValues.getValues(fieldName);
133+
if (fieldValues.length == 0) {
134+
fieldValues = new String[]{""};
137135
}
136+
for (String fieldValue : fieldValues) {
137+
AttributesImpl attrs = new AttributesImpl();
138+
attrs.addAttribute("", "fieldName", "fieldName", "CDATA", fieldName);
138139

139-
xhtml.startElement("li", attrs);
140-
xhtml.characters(sb.toString());
141-
xhtml.endElement("li");
142-
sb.setLength(0);
140+
sb.append(displayFieldName).append(": ");
141+
if (fieldValue != null) {
142+
sb.append(fieldValue);
143+
}
144+
145+
xhtml.startElement("li", attrs);
146+
xhtml.characters(sb.toString());
147+
xhtml.endElement("li");
148+
sb.setLength(0);
149+
}
143150
}
151+
xhtml.endElement("ol");
152+
xhtml.endElement("div");
153+
} finally {
154+
xhtml.endElement("div");
144155
}
145-
xhtml.endElement("ol");
146-
xhtml.endElement("div");
147-
xhtml.endElement("div");
148156
}
149157

150158
//try to scrape the text until the endElement

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,31 @@ public void testXFAOnly() throws Exception {
10371037
assertNotContained("Mount Rushmore National Memorial", xml);
10381038
}
10391039

1040+
@Test //TIKA-4744
1041+
public void testMalformedXFADivBalanced() throws Exception {
1042+
// A PDF whose XFA stream is malformed used to leave <div class=
1043+
// "xfa_content"> open: XFAExtractor.extract opened the div first and
1044+
// then threw XMLStreamException from reader.next(), the caller logged
1045+
// and fell through to AcroForm fallback (which emitted its own divs
1046+
// nested inside the unclosed xfa_content), and endDocument failed
1047+
// with </body> not matching topmost <div>.
1048+
// getXML wraps the handler in StrictXHTMLValidator, so any imbalance
1049+
// would throw before the assertions.
1050+
XMLResult r = getXML("testPDF_malformedXFA.pdf");
1051+
// Caller recorded the XMLStreamException as a warning rather than a
1052+
// fatal -- confirm that didn't change.
1053+
String[] warnings = r.metadata.getValues(
1054+
TikaCoreProperties.TIKA_META_EXCEPTION_WARNING);
1055+
boolean xfaWarningPresent = false;
1056+
for (String w : warnings) {
1057+
if (w.contains("XFAExtractor")) {
1058+
xfaWarningPresent = true;
1059+
break;
1060+
}
1061+
}
1062+
assertTrue(xfaWarningPresent, "expected an XFAExtractor warning");
1063+
}
1064+
10401065
@Test
10411066
public void testXMPMM() throws Exception {
10421067

0 commit comments

Comments
 (0)