Skip to content

Commit 6f6ee6f

Browse files
authored
Merge branch 'main' into metafile-rendering
2 parents 201ba73 + 6d66308 commit 6f6ee6f

9 files changed

Lines changed: 336 additions & 10 deletions

File tree

CHANGES.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ Release 4.1.0 - unreleased
1111
OLE2 formats (a WMF) as a THUMBNAIL embedded document, as the OOXML
1212
parsers do with the docProps thumbnail (TIKA-4855).
1313

14+
* Enum values in JSON configuration are matched case-insensitively, so
15+
"no_ocr" works as well as "NO_OCR"; the server docs used the lower-case
16+
form in their examples (TIKA-4859).
17+
18+
* The preview image of iWork '09 packages (QuickLook/Thumbnail.jpg) and of
19+
iWork '18 packages (preview.jpg) is emitted as a THUMBNAIL embedded
20+
document, as it already was for iWork '13 (TIKA-4854).
21+
1422
* Raw camera formats are detected by content: RawTiffDetector tells
1523
Nikon NEF/NRW, Pentax PEF/PTX, Sony ARW/SRF/SR2, Samsung SRW and Adobe
1624
DNG from a plain TIFF by their image directory (DNGVersion, the vendor

docs/modules/ROOT/pages/migration-to-4x/migrating-tika-server-4x.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ POST endpoints accept multipart requests with a `file` part and optional `config
6767
# Parse with custom PDF parser settings (requires allowPerRequestConfig)
6868
curl -X POST http://localhost:9998/tika/config/json \
6969
-F "file=@document.pdf" \
70-
-F "config={\"pdf-parser\":{\"ocr\":{\"strategy\":\"no_ocr\"}}};type=application/json"
70+
-F "config={\"pdf-parser\":{\"ocr\":{\"strategy\":\"NO_OCR\"}}};type=application/json"
7171
----
7272
7373
== Breaking Changes

docs/modules/ROOT/pages/using-tika/server/index.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ POST with multipart for custom per-request configuration:
246246
----
247247
curl -X POST http://localhost:9998/tika/config/json \
248248
-F "file=@document.pdf" \
249-
-F "config={\"pdf-parser\":{\"ocr\":{\"strategy\":\"no_ocr\"}}};type=application/json"
249+
-F "config={\"pdf-parser\":{\"ocr\":{\"strategy\":\"NO_OCR\"}}};type=application/json"
250250
----
251251

252252
Valid handler paths under `/tika/`: `text`, `html`, `xml`, `md`, `json` (bare `/tika` returns

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-apple-module/src/main/java/org/apache/tika/parser/iwork/IWorkPackageParser.java

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
3131
import org.apache.commons.compress.archivers.zip.ZipFile;
3232
import org.apache.commons.io.IOUtils;
33+
import org.apache.commons.io.input.BoundedInputStream;
3334
import org.apache.commons.io.input.CloseShieldInputStream;
3435
import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
3536
import org.xml.sax.ContentHandler;
@@ -38,9 +39,12 @@
3839
import org.apache.tika.annotation.TikaComponent;
3940
import org.apache.tika.detect.XmlRootExtractor;
4041
import org.apache.tika.exception.TikaException;
42+
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
43+
import org.apache.tika.extractor.EmbeddedDocumentUtil;
4144
import org.apache.tika.io.TikaInputStream;
4245
import org.apache.tika.metadata.HttpHeaders;
4346
import org.apache.tika.metadata.Metadata;
47+
import org.apache.tika.metadata.TikaCoreProperties;
4448
import org.apache.tika.mime.MediaType;
4549
import org.apache.tika.parser.ParseContext;
4650
import org.apache.tika.parser.Parser;
@@ -87,12 +91,48 @@ public Set<MediaType> getSupportedTypes(ParseContext context) {
8791
return supportedTypes;
8892
}
8993

94+
/**
95+
* The document preview of an iWork '09 package.
96+
*/
97+
public final static String IWORK_THUMBNAIL_ENTRY = "QuickLook/Thumbnail.jpg";
98+
99+
/**
100+
* Bound on the preview held in memory until the content has been
101+
* parsed; a real one is well under a megabyte.
102+
*/
103+
private static final long MAX_THUMBNAIL_BYTES = 20 * 1024 * 1024;
104+
90105
public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata,
91106
ParseContext context) throws IOException, SAXException, TikaException {
92107
ZipArchiveInputStream zip = new ZipArchiveInputStream(tis);
93108
ZipArchiveEntry entry = zip.getNextEntry();
109+
//the package is read as a stream, so the preview may come before the
110+
//content: hold it back and emit it once the content is written, and
111+
//only when the embedded document extractor wants it at all
112+
EmbeddedDocumentExtractor extractor =
113+
EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
114+
Metadata thumbnailMetadata = null;
115+
byte[] thumbnail = null;
116+
XHTMLContentHandler xhtml = null;
94117

95118
while (entry != null) {
119+
if (IWORK_THUMBNAIL_ENTRY.equals(entry.getName()) && zip.canReadEntryData(entry)) {
120+
thumbnailMetadata = thumbnailMetadata(context);
121+
if (extractor.shouldParseEmbedded(thumbnailMetadata, context)) {
122+
//read one byte past the limit so an oversized entry is
123+
//recognized and skipped instead of emitted truncated
124+
thumbnail = BoundedInputStream.builder().setInputStream(zip)
125+
.setMaxCount(MAX_THUMBNAIL_BYTES + 1).get().readAllBytes();
126+
if (thumbnail.length > MAX_THUMBNAIL_BYTES) {
127+
thumbnail = null;
128+
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
129+
IWORK_THUMBNAIL_ENTRY + " exceeds " + MAX_THUMBNAIL_BYTES
130+
+ " bytes and was skipped");
131+
}
132+
}
133+
entry = zip.getNextEntry();
134+
continue;
135+
}
96136
if (!IWORK_CONTENT_ENTRIES.contains(entry.getName())) {
97137
entry = zip.getNextEntry();
98138
continue;
@@ -104,7 +144,12 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
104144
entryStream.reset(); // 4096 fails on github
105145

106146
if (type != null) {
107-
XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context);
147+
if (xhtml == null) {
148+
//a package carries one content entry; guard against a
149+
//crafted one with several so the document is started once
150+
xhtml = new XHTMLContentHandler(handler, metadata, context);
151+
xhtml.startDocument();
152+
}
108153
ContentHandler contentHandler;
109154

110155
switch (type) {
@@ -126,19 +171,40 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
126171
}
127172

128173
metadata.set(HttpHeaders.CONTENT_TYPE, type.getType().toString());
129-
xhtml.startDocument();
130174
if (contentHandler != null) {
131175
XMLReaderUtils.parseSAX(CloseShieldInputStream.wrap(entryStream),
132176
contentHandler, context);
133177
}
134-
xhtml.endDocument();
135178
}
136179

137180
entry = zip.getNextEntry();
138181
}
182+
if (xhtml != null) {
183+
if (thumbnail != null) {
184+
try (TikaInputStream thumbnailStream = TikaInputStream.get(thumbnail)) {
185+
extractor.parseEmbedded(thumbnailStream, xhtml, thumbnailMetadata, context,
186+
true);
187+
}
188+
}
189+
xhtml.endDocument();
190+
}
139191
// Don't close the zip InputStream (TIKA-1117).
140192
}
141193

194+
/**
195+
* The metadata of the document preview, a
196+
* {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL} embedded
197+
* document.
198+
*/
199+
private static Metadata thumbnailMetadata(ParseContext context) {
200+
Metadata embeddedMetadata = Metadata.newInstance(context);
201+
embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
202+
TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString());
203+
embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, IWORK_THUMBNAIL_ENTRY);
204+
embeddedMetadata.set(HttpHeaders.CONTENT_TYPE, "image/jpeg");
205+
return embeddedMetadata;
206+
}
207+
142208
private IWORKDocumentType detectType(InputStream entryStream, int markLimit) throws IOException {
143209
byte[] bytes = new byte[markLimit];
144210
try {

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-apple-module/src/main/java/org/apache/tika/parser/iwork/iwana/IWork18PackageParser.java

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,28 @@
2525
import java.util.zip.ZipEntry;
2626
import java.util.zip.ZipInputStream;
2727

28+
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
2829
import org.apache.commons.compress.archivers.zip.ZipFile;
30+
import org.apache.commons.io.input.CloseShieldInputStream;
2931
import org.xml.sax.ContentHandler;
3032
import org.xml.sax.SAXException;
3133

3234
import org.apache.tika.annotation.TikaComponent;
3335
import org.apache.tika.exception.TikaException;
36+
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
37+
import org.apache.tika.extractor.EmbeddedDocumentUtil;
3438
import org.apache.tika.io.TikaInputStream;
3539
import org.apache.tika.metadata.HttpHeaders;
3640
import org.apache.tika.metadata.Metadata;
41+
import org.apache.tika.metadata.TikaCoreProperties;
3742
import org.apache.tika.mime.MediaType;
3843
import org.apache.tika.parser.ParseContext;
3944
import org.apache.tika.parser.Parser;
45+
import org.apache.tika.sax.XHTMLContentHandler;
4046

4147
/**
42-
* For now, this parser isn't even registered. It contains
43-
* code that will detect the newer 2018 .keynote, .numbers, .pages files.
48+
* Detects the newer 2018 .keynote, .numbers, .pages files and emits their
49+
* preview image as a thumbnail; the content itself is not parsed yet.
4450
*/
4551
@TikaComponent
4652
public class IWork18PackageParser implements Parser {
@@ -67,32 +73,74 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
6773
zipFile = (ZipFile) container;
6874
} else if (tis.hasFile()) {
6975
zipFile = ZipFile.builder().setFile(tis.getFile()).get();
76+
//closed with the stream, as the zip container detector does it
77+
tis.setOpenContainer(zipFile);
7078
} else {
7179
zipStream = new ZipInputStream(tis);
7280
}
7381

74-
// For now, just detect
82+
// Detect the type, and emit the document preview as the thumbnail;
83+
// the content itself is not parsed yet
84+
XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context);
85+
xhtml.startDocument();
7586
MediaType type = null;
7687
if (zipFile != null) {
77-
Enumeration<? extends ZipEntry> entries = zipFile.getEntries();
88+
Enumeration<? extends ZipArchiveEntry> entries = zipFile.getEntries();
7889
while (entries.hasMoreElements()) {
79-
ZipEntry entry = entries.nextElement();
90+
ZipArchiveEntry entry = entries.nextElement();
8091
if (type == null) {
8192
type = IWork18DocumentType.detectIfPossible(entry);
8293
}
94+
if (isPreview(entry) && zipFile.canReadEntryData(entry)) {
95+
try (TikaInputStream previewStream =
96+
TikaInputStream.get(zipFile.getInputStream(entry))) {
97+
handleThumbnail(entry, previewStream, xhtml, context);
98+
}
99+
}
83100
}
84101
} else {
85102
ZipEntry entry = zipStream.getNextEntry();
86103
while (entry != null) {
87104
if (type == null) {
88105
type = IWork18DocumentType.detectIfPossible(entry);
89106
}
107+
if (isPreview(entry)) {
108+
try (TikaInputStream previewStream =
109+
TikaInputStream.get(CloseShieldInputStream.wrap(zipStream))) {
110+
handleThumbnail(entry, previewStream, xhtml, context);
111+
}
112+
}
90113
entry = zipStream.getNextEntry();
91114
}
92115
}
93116
if (type != null) {
94117
metadata.set(HttpHeaders.CONTENT_TYPE, type.toString());
95118
}
119+
xhtml.endDocument();
120+
}
121+
122+
/**
123+
* The document preview, {@code preview.jpg} inside the package's
124+
* document directory (e.g. {@code Presentation.key/preview.jpg}).
125+
*/
126+
private static boolean isPreview(ZipEntry entry) {
127+
String name = entry.getName();
128+
return name.equals("preview.jpg") || name.endsWith("/preview.jpg");
129+
}
130+
131+
private static void handleThumbnail(ZipEntry entry, TikaInputStream previewStream,
132+
XHTMLContentHandler xhtml, ParseContext context)
133+
throws IOException, SAXException {
134+
EmbeddedDocumentExtractor extractor =
135+
EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
136+
Metadata embeddedMetadata = Metadata.newInstance(context);
137+
embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
138+
TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString());
139+
embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, entry.getName());
140+
embeddedMetadata.set(HttpHeaders.CONTENT_TYPE, "image/jpeg");
141+
if (extractor.shouldParseEmbedded(embeddedMetadata, context)) {
142+
extractor.parseEmbedded(previewStream, xhtml, embeddedMetadata, context, true);
143+
}
96144
}
97145

98146
public enum IWork18DocumentType {

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-apple-module/src/test/java/org/apache/tika/parser/iwork/IWorkParserTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,48 @@ public void setUp() {
5555
iWorkParser = new IWorkPackageParser();
5656
}
5757

58+
/**
59+
* The QuickLook preview of a Pages '09 package is the document's
60+
* THUMBNAIL embedded document, emitted after the content.
61+
*/
62+
@Test
63+
public void testPagesThumbnail() throws Exception {
64+
List<Metadata> metadataList = getRecursiveMetadata("testPages.pages");
65+
assertEquals(2, metadataList.size());
66+
assertEquals("application/vnd.apple.pages",
67+
metadataList.get(0).get(HttpHeaders.CONTENT_TYPE));
68+
Metadata thumbnail = metadataList.get(1);
69+
assertEquals("QuickLook/Thumbnail.jpg", thumbnail.get(TikaCoreProperties.RESOURCE_NAME_KEY));
70+
assertEquals("image/jpeg", thumbnail.get(HttpHeaders.CONTENT_TYPE));
71+
assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(),
72+
thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
73+
}
74+
75+
@Test
76+
public void testNumbersThumbnail() throws Exception {
77+
List<Metadata> metadataList = getRecursiveMetadata("testNumbers.numbers");
78+
assertEquals(2, metadataList.size());
79+
Metadata thumbnail = metadataList.get(1);
80+
assertEquals("QuickLook/Thumbnail.jpg", thumbnail.get(TikaCoreProperties.RESOURCE_NAME_KEY));
81+
assertEquals("image/jpeg", thumbnail.get(HttpHeaders.CONTENT_TYPE));
82+
assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(),
83+
thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
84+
}
85+
86+
/**
87+
* Keynote '09 also carries per-slide thumbnails under thumbs/; only the
88+
* QuickLook document preview is the thumbnail
89+
*/
90+
@Test
91+
public void testKeynoteThumbnail() throws Exception {
92+
List<Metadata> metadataList = getRecursiveMetadata("testKeynote.key");
93+
assertEquals(2, metadataList.size());
94+
assertEquals("QuickLook/Thumbnail.jpg",
95+
metadataList.get(1).get(TikaCoreProperties.RESOURCE_NAME_KEY));
96+
assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(),
97+
metadataList.get(1).get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
98+
}
99+
58100
/**
59101
* Check the given InputStream is not closed by the Parser (TIKA-1117).
60102
*

0 commit comments

Comments
 (0)