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/11 20:14:31 UTC
[tika] 01/01: TIKA-4012 -- improve document extraction in PDFs
This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch TIKA-4012
in repository https://gitbox.apache.org/repos/asf/tika.git
commit 884b1c92a41b409ec7a2c7e7714f3a0669f40708
Author: tallison <ta...@apache.org>
AuthorDate: Tue Apr 11 16:14:11 2023 -0400
TIKA-4012 -- improve document extraction in PDFs
---
CHANGES.txt | 2 +
.../main/java/org/apache/tika/metadata/PDF.java | 3 +
.../apache/tika/parser/pdf/AbstractPDF2XHTML.java | 155 ++++++++++-----------
3 files changed, 78 insertions(+), 82 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 36c880824..f20c6c596 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).
+
* Users may now avoid the ZeroByteFileException via a
setting on the AutoDetectParserConfig (TIKA-3976).
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..e68dd97d6 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,17 +47,16 @@ 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;
import org.apache.pdfbox.pdmodel.common.PDDestinationOrAction;
-import org.apache.pdfbox.pdmodel.common.PDNameTreeNode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification;
@@ -148,6 +146,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 +169,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 +338,37 @@ 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) {
- return;
- }
-
- Map<String, PDComplexFileSpecification> embeddedFileNames = new HashMap<>();
- int depth = 0;
- //recursively find embedded files
- extractFilesfromEFTree(efTree, embeddedFileNames, depth);
- processEmbeddedDocNames(embeddedFileNames);
+ //See 14.13.10 fo 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?).
- }
-
- 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");
- }
- Map<String, PDComplexFileSpecification> names = null;
- try {
- names = efTree.getNames();
- } catch (IOException e) {
- //LOG?
- }
- if (names != null) {
- for (Map.Entry<String, PDComplexFileSpecification> e : names.entrySet()) {
- embeddedFileNames.put(e.getKey(), e.getValue());
- }
+ List<COSObject> objs = document.getDocument().getObjectsByType(COSName.FILESPEC);
+ for (COSObject obj : objs) {
+ processDoc("", "", createFileSpecification(obj.getObject()), new AttributesImpl());
}
+ }
- 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 +384,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 +393,43 @@ 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 +439,22 @@ 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);
+ if (!StringUtils.isBlank(spec.getCOSObject().getString(AF_RELATIONSHIP))) {
+ embeddedMetadata.set(PDF.ASSOCIATED_FILE_RELATIONSHIP,
+ spec.getCOSObject().getString(AF_RELATIONSHIP));
}
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 +683,8 @@ class AbstractPDF2XHTML extends PDFTextStripper {
}
if (annotation instanceof PDAnnotationFileAttachment) {
PDAnnotationFileAttachment fann = (PDAnnotationFileAttachment) annotation;
- if (fann.getFile() instanceof PDComplexFileSpecification) {
- handlePDComplexFileSpec(fann.getAttachmentName(),
- "annotationFileAttachment",
- (PDComplexFileSpecification) fann.getFile());
- }
+ processDocOnAction("", "annotationFileAttachment", fann.getFile(),
+ new AttributesImpl());
} else if (annotation instanceof PDAnnotationWidget) {
handleWidget((PDAnnotationWidget) annotation);
} else {
@@ -717,8 +698,9 @@ class AbstractPDF2XHTML extends PDFTextStripper {
num3DAnnotations++;
}
for (COSDictionary fileSpec : findFileSpecs(annotation.getCOSObject())) {
- PDComplexFileSpecification cfs = new PDComplexFileSpecification(fileSpec);
- handlePDComplexFileSpec(cfs.getFilename(), annotationSubtype, cfs);
+ processDocOnAction("", annotationSubtype,
+ createFileSpecification(fileSpec),
+ new AttributesImpl());
}
}
// TODO: remove once PDFBOX-1143 is fixed:
@@ -908,17 +890,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 +1305,15 @@ class AbstractPDF2XHTML extends PDFTextStripper {
}
}
+ private PDFileSpecification createFileSpecification(COSBase cosBase) {
+ try {
+ return PDFileSpecification.createFS(cosBase);
+ } catch (IOException e) {
+ //swallow for now
+ }
+ return null;
+ }
+
enum ActionTrigger {
AFTER_DOCUMENT_PRINT, AFTER_DOCUMENT_SAVE, ANNOTATION_CURSOR_ENTERS, ANNOTATION_CURSOR_EXIT,
ANNOTATION_LOSE_INPUT_FOCUS, ANNOTATION_MOUSE_CLICK, ANNOTATION_MOUSE_RELEASED,