You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tika.apache.org by ta...@apache.org on 2023/04/13 19:03:17 UTC
[tika] branch main updated: TIKA-4012 -- improve extraction of embedded docs in PDFs by looking beyond names tree and annotations (#1079)
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 4dbaa08c4 TIKA-4012 -- improve extraction of embedded docs in PDFs by looking beyond names tree and annotations (#1079)
4dbaa08c4 is described below
commit 4dbaa08c41ed3bc24e930598076deb50d502d08b
Author: Tim Allison <ta...@apache.org>
AuthorDate: Thu Apr 13 15:03:12 2023 -0400
TIKA-4012 -- improve extraction of embedded docs in PDFs by looking beyond names tree and annotations (#1079)
* TIKA-4012 -- improve embedded document extraction in PDFs
---
CHANGES.txt | 2 +
.../main/java/org/apache/tika/metadata/PDF.java | 3 +
.../apache/tika/parser/pdf/AbstractPDF2XHTML.java | 235 +++++++++++++--------
.../org/apache/tika/parser/pdf/PDFParserTest.java | 1 -
4 files changed, 153 insertions(+), 88 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 4f424d520..c437ddaf1 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -8,6 +8,8 @@ Release 2.7.1 - ???
* Add extraction of rendition layout value and version from Epub (TIKA-4013).
+ * Improve embedded file extraction from PDFs (TIKA-4012).
+
* Update to PDFBox 2.0.28 (TIKA-4016).
* Users may now avoid the ZeroByteFileException via a
diff --git a/tika-core/src/main/java/org/apache/tika/metadata/PDF.java b/tika-core/src/main/java/org/apache/tika/metadata/PDF.java
index 1b96231c4..2b21e2590 100644
--- a/tika-core/src/main/java/org/apache/tika/metadata/PDF.java
+++ b/tika-core/src/main/java/org/apache/tika/metadata/PDF.java
@@ -175,4 +175,7 @@ public interface PDF {
* Number of 3D annotations a PDF contains. This makes {@link PDF#HAS_3D} redundant.
*/
Property NUM_3D_ANNOTATIONS = Property.internalInteger(PDF_PREFIX + "num3DAnnotations");
+
+ Property ASSOCIATED_FILE_RELATIONSHIP = Property.internalText(PDF_PREFIX +
+ "associatedFileRelationship");
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
index 4f2b2c864..f48bf204a 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/AbstractPDF2XHTML.java
@@ -35,7 +35,6 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
@@ -48,12 +47,12 @@ import javax.xml.stream.XMLStreamException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
+import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
-import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
-import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
@@ -148,6 +147,8 @@ class AbstractPDF2XHTML extends PDFTextStripper {
private static final String NULL_STRING = "null";
private static final MediaType XFA_MEDIA_TYPE = MediaType.application("vnd.adobe.xdp+xml");
private static final MediaType XMP_MEDIA_TYPE = MediaType.application("rdf+xml");
+
+ private static final COSName AF_RELATIONSHIP = COSName.getPDFName("AFRelationship");
final List<IOException> exceptions = new ArrayList<>();
final PDDocument pdDocument;
final XHTMLContentHandler xhtml;
@@ -169,6 +170,11 @@ class AbstractPDF2XHTML extends PDFTextStripper {
private final Set<String> triggers = new TreeSet<>();
private final Set<String> actionTypes = new TreeSet<>();
+
+ //these are files that we extract as part of Annotations
+ //We don't want to extract them twice when we go through the
+ //full DOM looking for /Type = /EmbeddedFile
+ private final Set<COSBase> extractedFiles = new HashSet<>();
//zero-based pageIndex
int pageIndex = 0;
int startPage = -1;
@@ -333,52 +339,69 @@ class AbstractPDF2XHTML extends PDFTextStripper {
private void extractEmbeddedDocuments(PDDocument document)
throws IOException, SAXException, TikaException {
- PDDocumentNameDictionary namesDictionary =
- new PDDocumentNameDictionary(document.getDocumentCatalog());
- PDEmbeddedFilesNameTreeNode efTree = namesDictionary.getEmbeddedFiles();
- if (efTree == null) {
+ //See 14.13.10 for the 2.0 spec. Associated files can show up in lots of places...even
+ // streams.
+ // It would be great to get more context from the /AF info, but we risk missing files
+ //if we don't look everywhere. With the current method, we're at least getting all
+ //filespecs at the cost of losing context (to what was this file attached: doc, page,
+ // stream, etc?).
+
+ //find all Filespecs TIKA-4012
+ List<COSObject> objs = document.getDocument().getObjectsByType(COSName.FILESPEC);
+ Set<COSBase> seen = new HashSet<>();
+ for (COSObject obj : objs) {
+ processDoc("", "", createFileSpecification(obj.getObject()), new AttributesImpl());
+ seen.add(obj.getObject());
+ }
+
+ //now go through the embedded files names tree to get those rare cases where
+ //a file (instead of a filespec) is attached directly to the names tree
+ //or where the filespec is a direct object
+
+ if (document.getDocumentCatalog() == null) {
return;
}
-
- Map<String, PDComplexFileSpecification> embeddedFileNames = new HashMap<>();
- int depth = 0;
- //recursively find embedded files
- extractFilesfromEFTree(efTree, embeddedFileNames, depth);
- processEmbeddedDocNames(embeddedFileNames);
-
- }
-
- private void extractFilesfromEFTree(PDNameTreeNode efTree,
- Map<String, PDComplexFileSpecification> embeddedFileNames,
- int depth) throws IOException {
- if (depth > MAX_RECURSION_DEPTH) {
- throw new IOException("Hit max recursion depth");
+ if (document.getDocumentCatalog().getNames() == null) {
+ return;
}
- Map<String, PDComplexFileSpecification> names = null;
- try {
- names = efTree.getNames();
- } catch (IOException e) {
- //LOG?
+ if (document.getDocumentCatalog().getNames().getEmbeddedFiles() == null) {
+ return;
}
- if (names != null) {
- for (Map.Entry<String, PDComplexFileSpecification> e : names.entrySet()) {
- embeddedFileNames.put(e.getKey(), e.getValue());
+ //use a list instead of a name-based map in case there are key collisions
+ //that could hide attachments
+ List<NameSpecTuple> specs = new ArrayList<>();
+ extractFilesfromEFTree(
+ document.getDocumentCatalog().getNames().getEmbeddedFiles(), specs, 0);
+ //this avoids duplication with the above /FileSpec searching, but also in the case
+ //where the same underlying file has different names in the EFTree
+ for (NameSpecTuple nameSpecTuple : specs) {
+ if (seen.contains(nameSpecTuple.getSpec().getCOSObject())) {
+ continue;
}
+ processDoc(nameSpecTuple.getName(), "", nameSpecTuple.getSpec(), new AttributesImpl());
+ seen.add(nameSpecTuple.getSpec().getCOSObject());
}
+ }
- List<PDNameTreeNode<PDComplexFileSpecification>> kids = efTree.getKids();
- if (kids == null) {
+ private void processDocOnAction(String name, String annotationType, PDFileSpecification spec,
+ AttributesImpl attributes)
+ throws TikaException, SAXException, IOException {
+ if (spec == null) {
return;
- } else {
- for (PDNameTreeNode<PDComplexFileSpecification> node : kids) {
- extractFilesfromEFTree(node, embeddedFileNames, depth + 1);
- }
}
+ processDoc(name, annotationType, spec, attributes);
+ extractedFiles.add(spec.getCOSObject());
}
private void processDoc(String name, String annotationType, PDFileSpecification spec,
AttributesImpl attributes)
throws TikaException, SAXException, IOException {
+ if (spec == null) {
+ return;
+ }
+ if (extractedFiles.contains(spec.getCOSObject())) {
+ return;
+ }
if (spec instanceof PDSimpleFileSpecification) {
//((PDSimpleFileSpecification)spec).getFile();
attributes.addAttribute("", "class", "class", "CDATA", "linked");
@@ -394,17 +417,6 @@ class AbstractPDF2XHTML extends PDFTextStripper {
}
}
- private void processEmbeddedDocNames(Map<String, PDComplexFileSpecification> embeddedFileNames)
- throws IOException, SAXException, TikaException {
- if (embeddedFileNames == null || embeddedFileNames.isEmpty()) {
- return;
- }
-
- for (Map.Entry<String, PDComplexFileSpecification> ent : embeddedFileNames.entrySet()) {
- processDoc(ent.getKey(), "", ent.getValue(), new AttributesImpl());
- }
- }
-
private void extractMultiOSPDEmbeddedFiles(String displayName,
String annotationType,
PDComplexFileSpecification spec,
@@ -414,42 +426,42 @@ class AbstractPDF2XHTML extends PDFTextStripper {
if (spec == null) {
return;
}
+
//current strategy is to pull all, not just first non-null
- extractPDEmbeddedFile(displayName, annotationType, spec.getFileUnicode(), spec.getFile(),
- spec.getFileDescription(), spec.getEmbeddedFile(), attributes);
- extractPDEmbeddedFile(displayName, annotationType, spec.getFileUnicode(), spec.getFileMac(),
- spec.getFileDescription(), spec.getEmbeddedFileMac(), attributes);
- extractPDEmbeddedFile(displayName, annotationType, spec.getFileUnicode(), spec.getFileDos(),
- spec.getFileDescription(), spec.getEmbeddedFileDos(), attributes);
- extractPDEmbeddedFile(displayName, annotationType, spec.getFileUnicode(),
- spec.getFileUnix(),
- spec.getFileDescription(), spec.getEmbeddedFileUnix(), attributes);
+ extractPDEmbeddedFile(displayName, annotationType, spec,
+ spec.getFile(), spec.getEmbeddedFile(), attributes);
+ extractPDEmbeddedFile(displayName, annotationType, spec,
+ spec.getFileMac(), spec.getEmbeddedFileMac(), attributes);
+ extractPDEmbeddedFile(displayName, annotationType, spec,
+ spec.getFileDos(), spec.getEmbeddedFileDos(), attributes);
+ extractPDEmbeddedFile(displayName, annotationType, spec,
+ spec.getFileUnix(), spec.getEmbeddedFileUnix(), attributes);
//Check for /Thumb (thumbnail image);
// /CI (collection item) adobe specific, can have /adobe:DisplayName and a summary
}
private void extractPDEmbeddedFile(String displayName,
- String annotationType, String unicodeFileName,
+ String annotationType,
+ PDComplexFileSpecification spec,
String fileName,
- String description, PDEmbeddedFile file,
+ PDEmbeddedFile pdEmbeddedFile,
AttributesImpl attributes)
- throws SAXException, IOException, TikaException {
+ throws SAXException, IOException {
- if (file == null) {
+ if (pdEmbeddedFile == null) {
//skip silently
return;
}
- fileName = (fileName == null || "".equals(fileName.trim())) ? unicodeFileName : fileName;
+ fileName = (fileName == null || "".equals(fileName.trim())) ? spec.getFileUnicode() : fileName;
fileName = (fileName == null || "".equals(fileName.trim())) ? displayName : fileName;
// TODO: other metadata?
Metadata embeddedMetadata = new Metadata();
embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, fileName);
- embeddedMetadata.set(Metadata.CONTENT_TYPE, file.getSubtype());
//if the stream is missing a size, -1 is returned
- long sz = file.getSize();
+ long sz = pdEmbeddedFile.getSize();
if (sz > -1) {
embeddedMetadata.set(Metadata.CONTENT_LENGTH, Long.toString(sz));
}
@@ -459,18 +471,25 @@ class AbstractPDF2XHTML extends PDFTextStripper {
if (!StringUtils.isBlank(annotationType)) {
embeddedMetadata.set(PDF.EMBEDDED_FILE_ANNOTATION_TYPE, annotationType);
}
- if (!StringUtils.isBlank(file.getSubtype())) {
- embeddedMetadata.set(PDF.EMBEDDED_FILE_SUBTYPE, file.getSubtype());
+ if (!StringUtils.isBlank(pdEmbeddedFile.getSubtype())) {
+ embeddedMetadata.set(PDF.EMBEDDED_FILE_SUBTYPE, pdEmbeddedFile.getSubtype());
+ }
+ if (!StringUtils.isBlank(spec.getFileDescription())) {
+ embeddedMetadata.set(PDF.EMBEDDED_FILE_DESCRIPTION, spec.getFileDescription());
}
- if (!StringUtils.isBlank(description)) {
- embeddedMetadata.set(PDF.EMBEDDED_FILE_DESCRIPTION, description);
+ String afRelationship = spec.getCOSObject().getNameAsString(AF_RELATIONSHIP);
+ if (StringUtils.isBlank(afRelationship)) {
+ afRelationship = spec.getCOSObject().getString(AF_RELATIONSHIP);
+ }
+ if (!StringUtils.isBlank(afRelationship)) {
+ embeddedMetadata.set(PDF.ASSOCIATED_FILE_RELATIONSHIP, afRelationship);
}
if (!embeddedDocumentExtractor.shouldParseEmbedded(embeddedMetadata)) {
return;
}
TikaInputStream stream = null;
try {
- stream = TikaInputStream.get(file.createInputStream());
+ stream = TikaInputStream.get(pdEmbeddedFile.createInputStream());
} catch (IOException e) {
//store this exception in the parent's metadata
EmbeddedDocumentUtil.recordEmbeddedStreamException(e, metadata);
@@ -699,11 +718,11 @@ class AbstractPDF2XHTML extends PDFTextStripper {
}
if (annotation instanceof PDAnnotationFileAttachment) {
PDAnnotationFileAttachment fann = (PDAnnotationFileAttachment) annotation;
- if (fann.getFile() instanceof PDComplexFileSpecification) {
- handlePDComplexFileSpec(fann.getAttachmentName(),
- "annotationFileAttachment",
- (PDComplexFileSpecification) fann.getFile());
- }
+ String subtype = "annotationFileAttachment";
+ AttributesImpl attributes = new AttributesImpl();
+ attributes.addAttribute("", "source", "source", "CDATA", subtype);
+ processDocOnAction("", subtype, fann.getFile(),
+ attributes);
} else if (annotation instanceof PDAnnotationWidget) {
handleWidget((PDAnnotationWidget) annotation);
} else {
@@ -717,8 +736,11 @@ class AbstractPDF2XHTML extends PDFTextStripper {
num3DAnnotations++;
}
for (COSDictionary fileSpec : findFileSpecs(annotation.getCOSObject())) {
- PDComplexFileSpecification cfs = new PDComplexFileSpecification(fileSpec);
- handlePDComplexFileSpec(cfs.getFilename(), annotationSubtype, cfs);
+ AttributesImpl attributes = new AttributesImpl();
+ attributes.addAttribute("", "source", "source", "CDATA", annotationSubtype);
+ processDocOnAction("", annotationSubtype,
+ createFileSpecification(fileSpec),
+ attributes);
}
}
// TODO: remove once PDFBOX-1143 is fixed:
@@ -821,20 +843,32 @@ class AbstractPDF2XHTML extends PDFTextStripper {
return PDFDOMUtil.findType(cosDict, types, MAX_RECURSION_DEPTH);
}
- private void handlePDComplexFileSpec(String attachmentName, String annotationType,
- PDComplexFileSpecification fileSpec) throws IOException {
+ private void extractFilesfromEFTree(PDNameTreeNode efTree,
+ List<NameSpecTuple> embeddedFileNames,
+ int depth) throws IOException {
+ if (depth > MAX_RECURSION_DEPTH) {
+ throw new IOException("Hit max recursion depth");
+ }
+ Map<String, PDComplexFileSpecification> names = null;
try {
- AttributesImpl attributes = new AttributesImpl();
- attributes.addAttribute("", "source", "source", "CDATA", annotationType);
- extractMultiOSPDEmbeddedFiles(attachmentName, annotationType, fileSpec, attributes);
- } catch (SAXException e) {
- throw new IOException("file embedded in annotation sax exception", e);
- } catch (TikaException e) {
- throw new IOException("file embedded in annotation tika exception", e);
+ names = efTree.getNames();
} catch (IOException e) {
- handleCatchableIOE(e);
+ //LOG?
+ }
+ if (names != null) {
+ for (Map.Entry<String, PDComplexFileSpecification> e : names.entrySet()) {
+ embeddedFileNames.add(new NameSpecTuple(e.getKey(), e.getValue()));
+ }
}
+ List<PDNameTreeNode<PDComplexFileSpecification>> kids = efTree.getKids();
+ if (kids == null) {
+ return;
+ } else {
+ for (PDNameTreeNode<PDComplexFileSpecification> node : kids) {
+ extractFilesfromEFTree(node, embeddedFileNames, depth + 1);
+ }
+ }
}
@@ -908,17 +942,17 @@ class AbstractPDF2XHTML extends PDFTextStripper {
addNonNullAttribute("trigger", actionTrigger.name(), attributes);
if (action instanceof PDActionImportData) {
- processDoc("", "", ((PDActionImportData) action).getFile(), attributes);
+ processDocOnAction("", "", ((PDActionImportData) action).getFile(), attributes);
} else if (action instanceof PDActionLaunch) {
PDActionLaunch pdActionLaunch = (PDActionLaunch) action;
addNonNullAttribute("id", pdActionLaunch.getF(), attributes);
addNonNullAttribute("defaultDirectory", pdActionLaunch.getD(), attributes);
addNonNullAttribute("operation", pdActionLaunch.getO(), attributes);
addNonNullAttribute("parameters", pdActionLaunch.getP(), attributes);
- processDoc(pdActionLaunch.getF(), "", pdActionLaunch.getFile(), attributes);
+ processDocOnAction(pdActionLaunch.getF(), "", pdActionLaunch.getFile(), attributes);
} else if (action instanceof PDActionRemoteGoTo) {
PDActionRemoteGoTo remoteGoTo = (PDActionRemoteGoTo) action;
- processDoc("", "", remoteGoTo.getFile(), attributes);
+ processDocOnAction("", "", remoteGoTo.getFile(), attributes);
} else if (action instanceof PDActionJavaScript) {
PDActionJavaScript jsAction = (PDActionJavaScript) action;
Metadata m = new Metadata();
@@ -1323,6 +1357,33 @@ class AbstractPDF2XHTML extends PDFTextStripper {
}
}
+ private PDFileSpecification createFileSpecification(COSBase cosBase) {
+ try {
+ return PDFileSpecification.createFS(cosBase);
+ } catch (IOException e) {
+ //swallow for now
+ }
+ return null;
+ }
+
+ private static class NameSpecTuple {
+ private final String name;
+ private final PDComplexFileSpecification spec;
+
+ public NameSpecTuple(String name, PDComplexFileSpecification spec) {
+ this.name = name;
+ this.spec = spec;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public PDComplexFileSpecification getSpec() {
+ return spec;
+ }
+ }
+
enum ActionTrigger {
AFTER_DOCUMENT_PRINT, AFTER_DOCUMENT_SAVE, ANNOTATION_CURSOR_ENTERS, ANNOTATION_CURSOR_EXIT,
ANNOTATION_LOSE_INPUT_FOCUS, ANNOTATION_MOUSE_CLICK, ANNOTATION_MOUSE_RELEASED,
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
index 7df87a171..ffa05f393 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
@@ -1433,5 +1433,4 @@ public class PDFParserTest extends TikaTest {
metadata.set(TikaCoreProperties.TIKA_CONTENT, contentHandler.toString());
return metadata;
}*/
-
}