This is an automated email from the ASF dual-hosted git repository.
tballison 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 5f743b7abb TIKA-4856: maxRenderedPages renders the first pages and
reads the whole document (#3146)
5f743b7abb is described below
commit 5f743b7abbd326e6562a836e9c07087f2a68accb
Author: Dominik Schmidt <[email protected]>
AuthorDate: Mon Sep 14 23:38:21 2026 +0300
TIKA-4856: maxRenderedPages renders the first pages and reads the whole
document (#3146)
---
CHANGES.txt | 5 ++
.../resources/config-examples/pdf-parser-full.json | 3 +
.../java/org/apache/tika/parser/pdf/PDF2XHTML.java | 5 ++
.../java/org/apache/tika/parser/pdf/PDFParser.java | 6 +-
.../apache/tika/parser/pdf/PDFParserConfig.java | 28 +++++++
.../tika/renderer/pdf/pdfbox/PDFBoxRenderer.java | 7 ++
.../tika/parser/pdf/PDFMaxRenderedPagesTest.java | 96 ++++++++++++++++++++++
.../renderer/pdf/pdfbox/PDFBoxRendererTest.java | 19 +++++
8 files changed, 167 insertions(+), 2 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index ba9ac60f3a..6c214ece66 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,10 @@
Release 4.1.0 - unreleased
+ * "pdf-parser": {"maxRenderedPages": N} bounds how many pages the
+ RENDER_PAGES_BEFORE_PARSE and RENDER_PAGES_AT_PAGE_END image strategies
+ render, counted from the first page and independent of maxPages: text
+ extraction can cover the whole document while only the first page is
+ rendered, as for a thumbnail (TIKA-4856).
* PDF: a bookmark outline is now walked without recursion, and lists nest at
most 50 deep with deeper items joining the deepest list (TIKA-4894).
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/config-examples/pdf-parser-full.json
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/config-examples/pdf-parser-full.json
index 55650f2a40..5024708e4b 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/config-examples/pdf-parser-full.json
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/config-examples/pdf-parser-full.json
@@ -57,6 +57,9 @@
"maxMainMemoryBytes": 536870912,
// Max pages to process; -1 = no limit.
"maxPages": -1,
+ // Max pages to render with RENDER_PAGES_BEFORE_PARSE /
RENDER_PAGES_AT_PAGE_END,
+ // counted from the first page and independent of maxPages; -1 = no
limit.
+ "maxRenderedPages": -1,
// OCR settings. Requires an OCR engine (e.g. Tesseract) installed.
"ocr": {
// Render resolution (dpi) for OCR.
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDF2XHTML.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDF2XHTML.java
index e23db89cc0..033619eddd 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDF2XHTML.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDF2XHTML.java
@@ -168,6 +168,11 @@ class PDF2XHTML extends AbstractPDF2XHTML {
if (config.getImageStrategy() !=
PDFParserConfig.IMAGE_STRATEGY.RENDER_PAGES_AT_PAGE_END) {
return;
}
+ // getCurrentPageNo() is 1-based, like PageRangeRequest: the first N
pages are 1..N
+ 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
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
index 97a277eef2..b5a273ce4f 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParser.java
@@ -491,8 +491,10 @@ public class PDFParser implements Parser, RenderingParser,
EnrichingParser {
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,
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParserConfig.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParserConfig.java
index 239f19196e..fafd5ff463 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParserConfig.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/parser/pdf/PDFParserConfig.java
@@ -151,6 +151,8 @@ public class PDFParserConfig implements Serializable {
private int maxPages = -1;
+ private int maxRenderedPages = -1;
+
private boolean throwOnEncryptedPayload = false;
/**
@@ -692,6 +694,32 @@ public class PDFParserConfig implements Serializable {
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 >= 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;
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRenderer.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRenderer.java
index 17d194abea..d00c6e1297 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRenderer.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRenderer.java
@@ -155,6 +155,13 @@ public class PDFBoxRenderer implements PDDocumentRenderer {
} else if (renderRequest instanceof PageRangeRequest) {
int start = ((PageRangeRequest)renderRequest).getFrom();
int toInclusive = ((PageRangeRequest)renderRequest).getTo();
+ int numberOfPages = pdDocument.getNumberOfPages();
+ // a range that runs past the last page ends there: "the first N
pages" of a
+ // shorter document are all of its pages. A range that starts past
the last
+ // page still asks for a page that does not exist, and getPage
throws on it.
+ if (start <= numberOfPages) {
+ toInclusive = Math.min(toInclusive, numberOfPages);
+ }
renderRange(pdDocument, start, toInclusive, metadata,
parseContext, results);
}
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFMaxRenderedPagesTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFMaxRenderedPagesTest.java
new file mode 100644
index 0000000000..7c55b95a15
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/parser/pdf/PDFMaxRenderedPagesTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.assertNull;
+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");
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = PDFParserConfig.IMAGE_STRATEGY.class,
+ names = {"RENDER_PAGES_BEFORE_PARSE", "RENDER_PAGES_AT_PAGE_END"})
+ public void
testLimitAbovePageCountRendersEveryPage(PDFParserConfig.IMAGE_STRATEGY strategy)
+ throws Exception {
+ PDFParserConfig config = new PDFParserConfig();
+ config.setImageStrategy(strategy);
+ config.setMaxRenderedPages(5);
+ ParseContext context = new ParseContext();
+ context.set(PDFParserConfig.class, config);
+
+ List<Metadata> metadataList = getRecursiveMetadata(TWO_PAGES, context);
+
assertNull(metadataList.get(0).get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING),
+ "a limit above the page count is not an error");
+ assertEquals(2, renderings(metadataList), "both pages, the limit is
not reached");
+ }
+
+ @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();
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRendererTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRendererTest.java
index a2de6211f9..030a7e5916 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRendererTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfbox/PDFBoxRendererTest.java
@@ -35,6 +35,7 @@ import org.junit.jupiter.api.parallel.Isolated;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaPagedText;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.pdf.PDFParserConfig;
import org.apache.tika.renderer.PageBasedRenderResults;
@@ -83,6 +84,24 @@ public class PDFBoxRendererTest {
"quality 1.0 should be far larger: " + uncompressed + " vs " +
compressed);
}
+ /** A range that runs past the last page ends there; it is "the first N
pages", not an error. */
+ @Test
+ public void testRangePastTheLastPageIsClamped() throws Exception {
+ PDFBoxRenderer renderer = new PDFBoxRenderer();
+ // testPDF_bookmarks.pdf has two pages
+ try (InputStream is =
getClass().getResourceAsStream("/test-documents/testPDF_bookmarks.pdf");
+ TikaInputStream tis = TikaInputStream.get(is);
+ PageBasedRenderResults results = (PageBasedRenderResults)
renderer.render(
+ tis, new Metadata(), new ParseContext(), new
PageRangeRequest(1, 9999))) {
+ assertEquals(2, results.getResults().size());
+ for (int i = 0; i < 2; i++) {
+ RenderResult r = results.getResults().get(i);
+ assertEquals(RenderResult.STATUS.SUCCESS, r.getStatus());
+ assertEquals(i + 1, (int)
r.getMetadata().getInt(TikaPagedText.PAGE_NUMBER));
+ }
+ }
+ }
+
/** An out-of-range page throws past the per-page IOException catch after
RENDER_ALL wrote pages. */
@Test
public void testFailedRenderLeavesNoTempFiles() throws Exception {