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 &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;
     }
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 {

Reply via email to