Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3563d72
TIKA-4856 - /unpack/thumbnail returns the document thumbnail with its…
dschmidt Aug 29, 2026
b40b3e7
TIKA-4856 - render the PDF page at 96 dpi, send the file name in the …
dschmidt Aug 29, 2026
1c0a756
TIKA-4856 - do not extract text for the thumbnail
dschmidt Aug 29, 2026
efd3ca9
TIKA-4856 - no OCR for the thumbnail request
dschmidt Aug 29, 2026
5c93b9a
TIKA-4856 - render only the thumbnail's metafile
dschmidt Aug 29, 2026
12a1135
TIKA-4856 - point the depth limit at TIKA-4857
dschmidt Aug 29, 2026
95f234b
TIKA-4856 - thumbnail defaults: renderThumbnails=true on /rmeta and /…
dschmidt Aug 29, 2026
7439050
Merge branch 'main' into unpack-thumbnail
dschmidt Aug 29, 2026
6a2e80b
TIKA-4856 - bound the thumbnail image at 32 MiB, 413 beyond
dschmidt Aug 29, 2026
bc2d9ef
TIKA-4856 - /unpack/thumbnail takes renderThumbnails too; rendering i…
dschmidt Aug 30, 2026
f161022
Merge branch 'main' into unpack-thumbnail
dschmidt Aug 30, 2026
1ee16cb
TIKA-4856 - render thumbnails in colour, not the grayscale OCR wants
dschmidt Aug 30, 2026
903a970
Merge branch 'main' into unpack-thumbnail
dschmidt Aug 30, 2026
f97c2a4
TIKA-4856 - NO_OCR defaults as a constant; test comment: raw formats …
dschmidt Aug 30, 2026
dca390c
Merge branch 'main' into unpack-thumbnail
dschmidt Aug 31, 2026
b263e27
Merge branch 'main' into unpack-thumbnail
dschmidt Aug 31, 2026
83b2b10
TIKA-4856 - maxDepth 2 now that TIKA-4857 is fixed
dschmidt Aug 31, 2026
8d0cdbd
Merge branch 'main' into unpack-thumbnail
dschmidt Aug 31, 2026
7e1084a
Merge branch 'main' into unpack-thumbnail
dschmidt Aug 31, 2026
a3d3079
Merge branch 'main' into unpack-thumbnail
dschmidt Aug 31, 2026
67afb5d
Merge branch 'main' into unpack-thumbnail
dschmidt Sep 1, 2026
8c20406
Merge branch 'main' into unpack-thumbnail
dschmidt Sep 1, 2026
9916407
Merge branch 'main' into unpack-thumbnail
dschmidt Sep 1, 2026
02d8646
TIKA-4856 - address review: merge the defaults as nodes, stop at the …
dschmidt Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
Release 4.1.0 - unreleased

* tika-server: /unpack/thumbnail returns the document thumbnail as JSON
(its metadata and the image as base64). renderThumbnails=true on /rmeta,
/unpack, /unpack/all and /unpack/thumbnail lays the thumbnail defaults
under a request (first PDF page rendered, EMF/WMF thumbnail rendered);
without it only stored thumbnails are found. The defaults can
be replaced per parser with a thumbnail-defaults block in the server
config. PDFParserConfig gains maxRenderedPages, bounding the page
rendering independently of maxPages (TIKA-4856).

* Raster previews for the vector thumbnails of Office documents: the new
poi-metafile-renderer draws EMF and WMF images through POI (a PNG of
a configurable width; Word's bitmap-in-WMF thumbnails from the bitmap
Expand Down
35 changes: 33 additions & 2 deletions docs/modules/ROOT/pages/using-tika/server/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,34 @@ curl -T document.docx http://localhost:9998/rmeta/markdown # or /md
curl -T document.pdf http://localhost:9998/rmeta/ignore # metadata only
----

The thumbnail of a document is one of its embedded documents, typed
`tk:embedded-resource-type=THUMBNAIL`. Most formats store one (Office, iWork, EPUB, DWG, the
cover art of audio files, the preview of a raw camera file) and it is listed without further
ado. Where a raster image only exists after rendering (the first page of a PDF, the EMF/WMF
thumbnail of an Office document), `?renderThumbnails=true` lays the *thumbnail defaults*
under the request: the first PDF page rendered at 96 dpi, the EMF/WMF thumbnail rendered (that
one only, not the pictures of embedded objects). The text is extracted as usual; the rendering
appears as an embedded document with its dimensions. The same switch works on `/unpack`,
`/unpack/all` and `/unpack/thumbnail`; without it only stored thumbnails are found.

[source,bash]
----
curl -T document.pdf "http://localhost:9998/rmeta/text?renderThumbnails=true"
----

The built-in defaults can be replaced per parser in the server config, with the same shape a
request config has; a request's own `config` part still wins over both:

[source,json]
----
{
"thumbnail-defaults": {
"pdf-parser": {"imageStrategy": "RENDER_PAGES_AT_PAGE_END", "maxRenderedPages": 1,
"ocr": {"dpi": 150}}
}
}
----

=== Metadata only (`/meta`)

Returns container-document metadata only — no recursive embedded list, no content. With no
Expand Down Expand Up @@ -310,9 +338,12 @@ enables everything below except `status`, which is opt-in and must be listed exp
|===
|Path |Config name |Notes

|`/unpack`
|`/unpack`, `/unpack/all`, `/unpack/thumbnail`
|`unpack`
|Returns embedded files as a zip. Forked.
|Returns embedded files as a zip; `/all` adds the container's text and metadata;
`/thumbnail` returns the document thumbnail as JSON (metadata plus the image as base64), or
204 if there is none. `?renderThumbnails=true` applies the thumbnail defaults on all three, see
<<_recursive_metadata_rmeta,`/rmeta`>>. Forked.

|`/detect`
|`detect`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ private void renderPage(PDPage page) throws IOException {
if (config.getImageStrategy() != PDFParserConfig.IMAGE_STRATEGY.RENDER_PAGES_AT_PAGE_END) {
return;
}
int maxRenderedPages = config.getMaxRenderedPages();
if (maxRenderedPages > 0 && getCurrentPageNo() > maxRenderedPages) {
return;
}
PDFRenderingState state = context.get(PDFRenderingState.class);
//this is the document's inputstream/PDDocument
//TODO: figure out if we can send in the PDPage in the TikaInputStream
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,10 @@ private RenderResults renderPDF(TikaInputStream tstream,
throws IOException, TikaException {
Metadata metadata = Metadata.newInstance(parseContext);
metadata.set(TikaCoreProperties.TYPE, MEDIA_TYPE.toString());
return renderer.render(
tstream, metadata, parseContext, PageRangeRequest.RENDER_ALL);
int maxRenderedPages = localConfig.getMaxRenderedPages();
PageRangeRequest pages = maxRenderedPages > 0
? new PageRangeRequest(1, maxRenderedPages) : PageRangeRequest.RENDER_ALL;
return renderer.render(tstream, metadata, parseContext, pages);
}

protected PDDocument getPDDocument(TikaInputStream tis, String password,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ public enum AccessCheckMode {

private int maxPages = -1;

private int maxRenderedPages = -1;

private boolean throwOnEncryptedPayload = false;

/**
Expand Down Expand Up @@ -681,6 +683,32 @@ public void setMaxPages(int maxPages) {
this.maxPages = maxPages;
}

/**
* @return maximum number of pages to render with the
* {@code RENDER_PAGES_BEFORE_PARSE} and {@code RENDER_PAGES_AT_PAGE_END}
* image strategies, or -1 for no limit
*/
public int getMaxRenderedPages() {
return maxRenderedPages;
}

/**
* Set the maximum number of pages to render, counted from the first
* page, independent of {@link #setMaxPages(int)}: text extraction can
* cover the whole document while only its first page is rendered, as
* for a thumbnail. Use -1 (the default) for no limit.
*
* @param maxRenderedPages must be -1 or &gt;= 1
* @throws IllegalArgumentException if the value is 0 or less than -1
*/
public void setMaxRenderedPages(int maxRenderedPages) {
if (maxRenderedPages != -1 && maxRenderedPages < 1) {
throw new IllegalArgumentException(
"maxRenderedPages must be -1 (no limit) or >= 1, got: " + maxRenderedPages);
}
this.maxRenderedPages = maxRenderedPages;
}

public void setThrowOnEncryptedPayload(boolean throwOnEncryptedPayload) {
this.throwOnEncryptedPayload = throwOnEncryptedPayload;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.parser.pdf;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import org.apache.tika.TikaTest;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.PagedText;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;

/**
* {@code maxRenderedPages} bounds the rendering without bounding the text:
* a thumbnail wants the first page rendered and the whole document read.
*/
public class PDFMaxRenderedPagesTest extends TikaTest {

private static final String TWO_PAGES = "testPDF_bookmarks.pdf";

@ParameterizedTest
@EnumSource(value = PDFParserConfig.IMAGE_STRATEGY.class,
names = {"RENDER_PAGES_BEFORE_PARSE", "RENDER_PAGES_AT_PAGE_END"})
public void testOnlyTheFirstPageIsRendered(PDFParserConfig.IMAGE_STRATEGY strategy)
throws Exception {
PDFParserConfig config = new PDFParserConfig();
config.setImageStrategy(strategy);
config.setMaxRenderedPages(1);
ParseContext context = new ParseContext();
context.set(PDFParserConfig.class, config);

List<Metadata> metadataList = getRecursiveMetadata(TWO_PAGES, context);
assertEquals(2, (int) metadataList.get(0).getInt(PagedText.N_PAGES));
assertEquals(1, renderings(metadataList), "one rendering, the first page");
}

@Test
public void testJsonConfig() throws Exception {
ParseContext context = new ParseContext();
context.setJsonConfig("pdf-parser",
"{\"imageStrategy\": \"RENDER_PAGES_AT_PAGE_END\", \"maxRenderedPages\": 1}");
assertEquals(1, renderings(getRecursiveMetadata(TWO_PAGES, context)));
}

@Test
public void testZeroIsRejected() {
assertThrows(IllegalArgumentException.class,
() -> new PDFParserConfig().setMaxRenderedPages(0));
}

private static long renderings(List<Metadata> metadataList) {
return metadataList.stream()
.filter(m -> TikaCoreProperties.EmbeddedResourceType.RENDERING.name()
.equals(m.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)))
.count();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public class TikaJsonConfig {
"auto-detect-parser",
"parse-context",
"server",
"thumbnail-defaults",
"grpc",

// Pipes/plugin keys
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedMap;
Expand Down Expand Up @@ -63,8 +64,23 @@ public List<Metadata> parseMetadata(TikaInputStream tis, Metadata metadata,
MultivaluedMap<String, String> httpHeaders,
String handlerTypeName)
throws Exception {
return parseMetadata(tis, metadata, httpHeaders, handlerTypeName, false);
}

/**
* @param renderThumbnails whether to lay the {@link ThumbnailDefaults} under the
* request, so the parse yields the document thumbnail as
* a raster image among the embedded documents
*/
public List<Metadata> parseMetadata(TikaInputStream tis, Metadata metadata,
MultivaluedMap<String, String> httpHeaders,
String handlerTypeName, boolean renderThumbnails)
throws Exception {

final ParseContext context = tikaResource.createRequestContext();
if (renderThumbnails) {
tikaResource.getThumbnailDefaults().applyTo(context);
}

fillMetadata(null, metadata, httpHeaders);
TikaResource.logRequest(LOG, "/rmeta", metadata);
Expand Down Expand Up @@ -104,10 +120,11 @@ public List<Metadata> parseMetadata(TikaInputStream tis, Metadata metadata,
@Consumes("multipart/form-data")
@Produces({"application/json"})
@Path("form{" + HANDLER_TYPE_PARAM + " : (\\w+)?}")
public Response getMetadataFromMultipart(Attachment att, @PathParam(HANDLER_TYPE_PARAM) String handlerTypeName) throws Exception {
public Response getMetadataFromMultipart(Attachment att, @PathParam(HANDLER_TYPE_PARAM) String handlerTypeName,
@QueryParam("renderThumbnails") boolean renderThumbnails) throws Exception {
try (TikaInputStream tis = TikaInputStream.get(att.getObject(InputStream.class))) {
List<Metadata> metadataList = parseMetadata(tis, tikaResource.newRequestMetadata(), att.getHeaders(),
handlerTypeName);
handlerTypeName, renderThumbnails);
return Response.ok(new MetadataList(metadataList)).build();
}
}
Expand All @@ -123,11 +140,16 @@ public Response getMetadataFromMultipart(Attachment att, @PathParam(HANDLER_TYPE
@Path("config")
public Response getMetadataWithConfig(
List<Attachment> attachments,
@Context HttpHeaders httpHeaders) throws Exception {
@Context HttpHeaders httpHeaders,
@QueryParam("renderThumbnails") boolean renderThumbnails) throws Exception {

ParseContext context = tikaResource.createRequestContext();
Metadata metadata = tikaResource.newRequestMetadata();
try (TikaInputStream tis = tikaResource.setupMultipartConfig(attachments, metadata, context)) {
if (renderThumbnails) {
//under the request's config, which setupMultipartConfig has already merged
tikaResource.getThumbnailDefaults().applyTo(context);
}

TikaResource.logRequest(LOG, "/rmeta/config", metadata);

Expand Down Expand Up @@ -164,6 +186,11 @@ private MetadataList parseMetadataWithContext(TikaInputStream tis, Metadata meta
* /rmeta/text (store the content as text)<br/>
* /rmeta/markdown (store the content as markdown)<br/>
* /rmeta/ignore (don't record any content)<br/>
* <p>
* With {@code ?renderThumbnails=true} the {@link ThumbnailDefaults} are laid
* under the request, so the list also holds the document thumbnail as a raster
* image where rendering is needed for that (the first PDF page, the EMF/WMF
* thumbnail of an Office document).
*
* @param handlerTypeName which type of handler to use
* @return InputStream that can be deserialized as a list of {@link Metadata} objects
Expand All @@ -173,11 +200,12 @@ private MetadataList parseMetadataWithContext(TikaInputStream tis, Metadata meta
@PUT
@Produces("application/json")
@Path("{" + HANDLER_TYPE_PARAM + " : (\\w+)?}")
public Response getMetadata(InputStream is, @Context HttpHeaders httpHeaders, @PathParam(HANDLER_TYPE_PARAM) String handlerTypeName) throws Exception {
public Response getMetadata(InputStream is, @Context HttpHeaders httpHeaders, @PathParam(HANDLER_TYPE_PARAM) String handlerTypeName,
@QueryParam("renderThumbnails") boolean renderThumbnails) throws Exception {
Metadata metadata = tikaResource.newRequestMetadata();
try (TikaInputStream tis = TikaInputStream.get(is)) {
List<Metadata> metadataList = parseMetadata(tis, metadata, httpHeaders.getRequestHeaders(),
handlerTypeName);
handlerTypeName, renderThumbnails);
return Response.ok(new MetadataList(metadataList)).build();
}
}
Expand Down
Loading
Loading