This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch pdfiumtoppm-renderer
in repository https://gitbox.apache.org/repos/asf/tika.git

commit be3b902cfcfb190be48b29632ce52c8315dd3a98
Author: tallison <[email protected]>
AuthorDate: Fri Aug 28 20:05:35 2026 -0400

    WIP - add PdfiumToPpmRenderer
---
 CHANGES.txt                                        |   7 +
 .../org/apache/tika/parser/pdf/PDFParserTest.java  |  11 +
 .../configs/tika-rendering-pdfiumtoppm-config.json |  17 +
 .../pdf/pdfiumtoppm/PdfiumToPpmRenderer.java       | 374 +++++++++++++++++++++
 .../pdf/pdfiumtoppm/PdfiumToPpmRendererTest.java   | 221 ++++++++++++
 5 files changed, 630 insertions(+)

diff --git a/CHANGES.txt b/CHANGES.txt
index c4d49ce9cb..e11c32f596 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,12 @@
 Release 4.1.0 - unreleased
 
+   * New pdfiumtoppm-renderer for PDF page rendering and OCR: drives the
+     external pdfiumtoppm command (PDFium behind a pdftoppm-compatible
+     command line) with -max-pixels and -max-memory limits for untrusted
+     input; exit 4 surfaces as TikaMemoryLimitException. Output is PNG, or
+     PPM/PGM with png=false (no encode/decode; pair with the OCR engine's
+     explicit DPI, e.g. tesseract user_defined_dpi).
+
    * New exception-reporting parse-context config controls how much of an
      exception is written to tk:exception:* metadata for the container and
      embedded documents alike: FULL (default), MESSAGE_REDACTED (stack trace
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
index 0c8160aced..f04b64311b 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/pdf/PDFParserTest.java
@@ -467,6 +467,17 @@ public class PDFParserTest extends TikaTest {
         assertContains("Happy", text.trim());
     }
 
+    @Test
+    public void testPdfiumToPpmInOCR() throws Exception {
+        assumeTrue(canRunOCR(), "can't run OCR");
+        assumeTrue(ProcessUtils.checkCommand(new String[]{"pdfiumtoppm", 
"-v"}),
+                "does not have pdfiumtoppm");
+        Parser p = 
TikaLoaderHelper.getLoader("tika-rendering-pdfiumtoppm-config.json")
+                .loadAutoDetectParser();
+        String text = 
getText(getResourceAsStream("/test-documents/testOCR.pdf"), p);
+        assertContains("Happy", text.trim());
+    }
+
     @Test
     public void testIncrementalUpdatesInAnAttachedPDF() throws Exception {
         PDFParserConfig pdfParserConfig = new PDFParserConfig();
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/configs/tika-rendering-pdfiumtoppm-config.json
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/configs/tika-rendering-pdfiumtoppm-config.json
new file mode 100644
index 0000000000..5fc725f876
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/configs/tika-rendering-pdfiumtoppm-config.json
@@ -0,0 +1,17 @@
+{
+  "parsers": [
+    {
+      "default-parser":{}
+    },
+    {
+      "tesseract-ocr-parser": {
+        "pageSegMode": 3
+      }
+    }
+  ],
+  "renderers": [
+    {
+      "pdfiumtoppm-renderer": {}
+    }
+  ]
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRenderer.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRenderer.java
new file mode 100644
index 0000000000..66dfe90a44
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/main/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRenderer.java
@@ -0,0 +1,374 @@
+/*
+ * 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.renderer.pdf.pdfiumtoppm;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.exception.TikaMemoryLimitException;
+import org.apache.tika.exception.TikaTimeoutException;
+import org.apache.tika.io.TemporaryResources;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.metadata.TikaPagedText;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.renderer.PageBasedRenderResults;
+import org.apache.tika.renderer.PageRangeRequest;
+import org.apache.tika.renderer.RenderRequest;
+import org.apache.tika.renderer.RenderResult;
+import org.apache.tika.renderer.RenderResults;
+import org.apache.tika.renderer.Renderer;
+import org.apache.tika.renderer.RenderingTracker;
+import org.apache.tika.utils.FileProcessResult;
+import org.apache.tika.utils.ProcessUtils;
+
+/**
+ * Renderer that uses the {@code pdfiumtoppm} command
+ * (<a 
href="https://github.com/tballison/pdfiumtoppm";>github.com/tballison/pdfiumtoppm</a>),
+ * a {@code pdftoppm}-compatible renderer built on PDFium, to convert PDF pages
+ * to PNG images.
+ * <p>
+ * Compared with {@code pdftoppm} it renders faster with a shorter tail on
+ * pathological files and adds hard limits for untrusted input:
+ * {@code -max-pixels} (downscale only, never enlarge) and {@code -max-memory}
+ * (an address-space limit the process enforces on itself; exit code 4 when
+ * hit). The binary and a matching {@code libpdfium.so} ship together in the
+ * release tarball; keep them in the same directory.
+ * <p>
+ * Configuration key: {@code "pdfiumtoppm-renderer"}
+ *
+ * @since Apache Tika 4.1
+ */
+@TikaComponent(name = "pdfiumtoppm-renderer", spi = false)
+public class PdfiumToPpmRenderer implements Renderer {
+
+    /** pdfiumtoppm exits 4 when a page could not fit under {@code 
-max-memory}. */
+    static final int EXIT_MEMORY = 4;
+
+    private static final Set<MediaType> SUPPORTED_TYPES =
+            Collections.singleton(MediaType.application("pdf"));
+
+    private static final Pattern PAGE_FILE_PATTERN =
+            Pattern.compile("tika-pdfium-(\\d+)\\.(png|ppm|pgm)");
+
+    private String pdfiumToPpmPath = "pdfiumtoppm";
+    private String pdfiumLibraryDir = null;
+    private int dpi = 300;
+    private boolean gray = true;
+
+    /**
+     * Write PNG (default) or, when false, binary PPM/PGM: no encode or 
decode, but no DPI
+     * header either, so tell the OCR engine the resolution (tesseract: {@code 
user_defined_dpi}).
+     */
+    private boolean png = true;
+    private int timeoutMillis = 120000;
+
+    /**
+     * Longest edge in pixels; maps to {@code -scale-to}. Like {@code 
pdftoppm},
+     * this scales every page to exactly this size, enlarging small pages and
+     * ignoring {@code dpi}. Off by default; {@link #maxPixels} is the cap.
+     */
+    private int maxScaleTo = -1;
+
+    /**
+     * Maximum width &times; height of a rendered page; maps to
+     * {@code -max-pixels}. Pages that would exceed it are downscaled to fit;
+     * smaller pages are untouched. Default 16,777,216 (4096 &times; 4096).
+     */
+    private long maxPixels = 4096L * 4096L;
+
+    /**
+     * Address-space limit in MiB for the render process; maps to
+     * {@code -max-memory}. {@code -1} leaves pdfiumtoppm's own default
+     * (4096 MiB or half of RAM, whichever is lower); {@code 0} disables the
+     * limit.
+     */
+    private int maxMemoryMb = -1;
+
+    @Override
+    public Set<MediaType> getSupportedTypes(ParseContext context) {
+        return SUPPORTED_TYPES;
+    }
+
+    @Override
+    public RenderResults render(TikaInputStream tis, Metadata metadata,
+                                ParseContext parseContext,
+                                RenderRequest... requests)
+            throws IOException, TikaException {
+        TemporaryResources tmp = new TemporaryResources();
+        PageBasedRenderResults results = new PageBasedRenderResults(tmp);
+        Path path = tis.getPath();
+        for (RenderRequest request : requests) {
+            renderRequest(path, parseContext, request, results, tmp);
+        }
+        return results;
+    }
+
+    private void renderRequest(Path pdf, ParseContext parseContext,
+                               RenderRequest request,
+                               PageBasedRenderResults results,
+                               TemporaryResources tmp)
+            throws TikaException, IOException {
+        if (!(request instanceof PageRangeRequest)) {
+            throw new TikaException(
+                    "I regret that this renderer can only handle "
+                            + "PageRangeRequests, not " + request.getClass());
+        }
+        PageRangeRequest rangeRequest = (PageRangeRequest) request;
+
+        RenderingTracker tracker = parseContext.get(RenderingTracker.class);
+        if (tracker == null) {
+            tracker = new RenderingTracker();
+            parseContext.set(RenderingTracker.class, tracker);
+        }
+
+        Path dir = Files.createTempDirectory("tika-render-");
+        tmp.addResource(new Closeable() {
+            @Override
+            public void close() throws IOException {
+                Files.delete(dir);
+            }
+        });
+
+        String[] args = createCommandLine(pdf, dir, rangeRequest);
+
+        ProcessBuilder builder = new ProcessBuilder();
+        builder.command(args);
+        FileProcessResult result = ProcessUtils.execute(
+                builder, parseContext, timeoutMillis, 10, 1000);
+        if (result.isTimeout()) {
+            throw new TikaTimeoutException("pdfiumtoppm timed out",
+                    result.getRequestedTimeoutMillis(), 
result.getGrantedTimeoutMillis());
+        } else if (result.getExitValue() == EXIT_MEMORY) {
+            throw new TikaMemoryLimitException(
+                    "pdfiumtoppm hit its -max-memory limit: " + 
result.getStderr());
+        } else if (result.getExitValue() != 0) {
+            throw new TikaException(
+                    "pdfiumtoppm failed (exit " + result.getExitValue()
+                            + "): " + result.getStderr());
+        }
+
+        Matcher m = PAGE_FILE_PATTERN.matcher("");
+        File[] files = dir.toFile().listFiles();
+        if (files == null) {
+            return;
+        }
+        for (File f : files) {
+            if (m.reset(f.getName()).find()) {
+                int pageNumber = Integer.parseInt(m.group(1));
+                Metadata renderMetadata = Metadata.newInstance(parseContext);
+                renderMetadata.set(TikaPagedText.PAGE_NUMBER, pageNumber);
+                renderMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+                        TikaCoreProperties.EmbeddedResourceType.RENDERING
+                                .name());
+                results.add(new RenderResult(
+                        RenderResult.STATUS.SUCCESS,
+                        tracker.getNextId(),
+                        f.toPath(),
+                        renderMetadata));
+            }
+        }
+    }
+
+    String[] createCommandLine(Path pdf, Path dir,
+                               PageRangeRequest request) {
+        List<String> args = new ArrayList<>();
+        args.add(pdfiumToPpmPath);
+        if (pdfiumLibraryDir != null) {
+            args.add("-pdfium");
+            args.add(ProcessUtils.escapeCommandLine(pdfiumLibraryDir));
+        }
+        if (png) {
+            args.add("-png");
+        }
+        args.add("-r");
+        args.add(String.valueOf(dpi));
+        if (maxScaleTo > 0) {
+            args.add("-scale-to");
+            args.add(String.valueOf(maxScaleTo));
+        }
+        if (maxPixels > 0) {
+            args.add("-max-pixels");
+            args.add(String.valueOf(maxPixels));
+        }
+        if (maxMemoryMb >= 0) {
+            args.add("-max-memory");
+            args.add(String.valueOf(maxMemoryMb));
+        }
+        if (gray) {
+            args.add("-gray");
+        }
+        if (request != PageRangeRequest.RENDER_ALL) {
+            args.add("-f");
+            args.add(String.valueOf(request.getFrom()));
+            args.add("-l");
+            args.add(String.valueOf(request.getTo()));
+        }
+        args.add(ProcessUtils.escapeCommandLine(
+                pdf.toAbsolutePath().toString()));
+        args.add(ProcessUtils.escapeCommandLine(
+                dir.toAbsolutePath().toString() + "/tika-pdfium"));
+        return args.toArray(new String[0]);
+    }
+
+    // ---- config getters/setters -------------------------------------------
+
+    public String getPdfiumToPpmPath() {
+        return pdfiumToPpmPath;
+    }
+
+    /**
+     * Path to the {@code pdfiumtoppm} executable. Defaults to
+     * {@code "pdfiumtoppm"} (on the system path).
+     */
+    public void setPdfiumToPpmPath(String pdfiumToPpmPath) {
+        this.pdfiumToPpmPath = pdfiumToPpmPath;
+    }
+
+    public String getPdfiumLibraryDir() {
+        return pdfiumLibraryDir;
+    }
+
+    /**
+     * Directory containing {@code libpdfium.so}; maps to {@code -pdfium}.
+     * Optional: by default pdfiumtoppm looks in {@code $PDFIUM_PATH}, its
+     * own directory, then the system library path.
+     */
+    public void setPdfiumLibraryDir(String pdfiumLibraryDir) {
+        this.pdfiumLibraryDir = pdfiumLibraryDir;
+    }
+
+    public int getDpi() {
+        return dpi;
+    }
+
+    /**
+     * Rendering resolution in DPI. Defaults to 300. Ignored when
+     * {@link #setMaxScaleTo(int)} is set.
+     */
+    public void setDpi(int dpi) {
+        if (dpi < 1) {
+            throw new IllegalArgumentException("dpi must be at least 1, got: " 
+ dpi);
+        }
+        this.dpi = dpi;
+    }
+
+    public boolean isPng() {
+        return png;
+    }
+
+    /**
+     * If true (the default), pages are written as PNG. If false, as binary 
PPM (PGM with
+     * {@link #setGray(boolean)}), which skips PNG encode/decode; the images 
then carry no DPI,
+     * so set the OCR engine's resolution explicitly.
+     */
+    public void setPng(boolean png) {
+        this.png = png;
+    }
+
+    public boolean isGray() {
+        return gray;
+    }
+
+    /**
+     * If true (the default), render in grayscale.
+     */
+    public void setGray(boolean gray) {
+        this.gray = gray;
+    }
+
+    public int getTimeoutMillis() {
+        return timeoutMillis;
+    }
+
+    /**
+     * Timeout in milliseconds for the pdfiumtoppm process. Defaults to
+     * 120000 (2 minutes). pdfiumtoppm bounds memory but not time; this is
+     * the only time limit.
+     */
+    public void setTimeoutMillis(int timeoutMillis) {
+        this.timeoutMillis = timeoutMillis;
+    }
+
+    public int getMaxScaleTo() {
+        return maxScaleTo;
+    }
+
+    /**
+     * Scale every page's longest edge to exactly this many pixels
+     * ({@code -scale-to}); enlarges small pages and overrides {@code dpi}.
+     * {@code -1} (the default) disables it. Prefer {@link #setMaxPixels(long)}
+     * for a cap that never enlarges.
+     */
+    public void setMaxScaleTo(int maxScaleTo) {
+        if (maxScaleTo < 1 && maxScaleTo != -1) {
+            throw new IllegalArgumentException(
+                    "maxScaleTo must be -1 (disabled) or at least 1, got: "
+                            + maxScaleTo);
+        }
+        this.maxScaleTo = maxScaleTo;
+    }
+
+    public long getMaxPixels() {
+        return maxPixels;
+    }
+
+    /**
+     * Maximum width &times; height of a rendered page ({@code -max-pixels});
+     * larger pages are downscaled to fit. Default 16,777,216. {@code -1}
+     * disables it (not recommended).
+     */
+    public void setMaxPixels(long maxPixels) {
+        if (maxPixels < 1 && maxPixels != -1) {
+            throw new IllegalArgumentException(
+                    "maxPixels must be -1 (disabled) or at least 1, got: " + 
maxPixels);
+        }
+        this.maxPixels = maxPixels;
+    }
+
+    public int getMaxMemoryMb() {
+        return maxMemoryMb;
+    }
+
+    /**
+     * Address-space limit in MiB for the render process ({@code -max-memory}).
+     * {@code -1} (the default) keeps pdfiumtoppm's own default of 4096 MiB or
+     * half of RAM, whichever is lower; {@code 0} disables the limit. When the
+     * limit is hit the render fails with a {@link TikaMemoryLimitException}.
+     */
+    public void setMaxMemoryMb(int maxMemoryMb) {
+        if (maxMemoryMb < -1) {
+            throw new IllegalArgumentException(
+                    "maxMemoryMb must be -1 (tool default), 0 (no limit) or 
positive, got: "
+                            + maxMemoryMb);
+        }
+        this.maxMemoryMb = maxMemoryMb;
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRendererTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRendererTest.java
new file mode 100644
index 0000000000..398a425626
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pdf-module/src/test/java/org/apache/tika/renderer/pdf/pdfiumtoppm/PdfiumToPpmRendererTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.renderer.pdf.pdfiumtoppm;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.exception.TikaMemoryLimitException;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.metadata.TikaPagedText;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.renderer.PageBasedRenderResults;
+import org.apache.tika.renderer.PageRangeRequest;
+import org.apache.tika.renderer.RenderResult;
+import org.apache.tika.utils.ProcessUtils;
+
+public class PdfiumToPpmRendererTest {
+
+    private static boolean hasPdfiumToPpm;
+
+    @BeforeAll
+    static void checkPdfiumToPpm() {
+        hasPdfiumToPpm = ProcessUtils.checkCommand(new String[]{"pdfiumtoppm", 
"-v"});
+    }
+
+    @Test
+    void testCommandLineDefaults() {
+        PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer();
+        String[] args = renderer.createCommandLine(Path.of("/in/a.pdf"), 
Path.of("/out"),
+                PageRangeRequest.RENDER_ALL);
+        assertArrayEquals(new String[]{"pdfiumtoppm", "-png", "-r", "300",
+                "-max-pixels", "16777216", "-gray", "/in/a.pdf", 
"/out/tika-pdfium"}, args);
+    }
+
+    @Test
+    void testCommandLineAllOptions() {
+        PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer();
+        renderer.setPdfiumToPpmPath("/opt/pdfiumtoppm/pdfiumtoppm");
+        renderer.setPdfiumLibraryDir("/opt/pdfiumtoppm");
+        renderer.setDpi(72);
+        renderer.setGray(false);
+        renderer.setMaxScaleTo(2048);
+        renderer.setMaxPixels(-1);
+        renderer.setMaxMemoryMb(1024);
+        String[] args = renderer.createCommandLine(Path.of("/in/a.pdf"), 
Path.of("/out"),
+                new PageRangeRequest(2, 3));
+        assertArrayEquals(new String[]{"/opt/pdfiumtoppm/pdfiumtoppm", 
"-pdfium",
+                "/opt/pdfiumtoppm", "-png", "-r", "72", "-scale-to", "2048", 
"-max-memory",
+                "1024", "-f", "2", "-l", "3", "/in/a.pdf", 
"/out/tika-pdfium"}, args);
+    }
+
+    @Test
+    void testPpmOutput() throws Exception {
+        PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer();
+        renderer.setPng(false);
+        List<String> args = 
List.of(renderer.createCommandLine(Path.of("/in/a.pdf"),
+                Path.of("/out"), PageRangeRequest.RENDER_ALL));
+        assertTrue(!args.contains("-png"), args.toString());
+
+        assumeTrue(hasPdfiumToPpm, "pdfiumtoppm not available");
+        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, 1));
+            assertEquals(1, results.getResults().size());
+            try (TikaInputStream img = 
results.getResults().get(0).getInputStream()) {
+                byte[] head = img.readNBytes(2);
+                assertEquals("P5", new String(head, 
java.nio.charset.StandardCharsets.US_ASCII), "gray -> PGM");
+            }
+            results.close();
+        }
+    }
+
+    @Test
+    void testMemoryZeroDisablesLimit() {
+        PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer();
+        renderer.setMaxMemoryMb(0);
+        List<String> args = 
List.of(renderer.createCommandLine(Path.of("/in/a.pdf"),
+                Path.of("/out"), PageRangeRequest.RENDER_ALL));
+        assertEquals("0", args.get(args.indexOf("-max-memory") + 1));
+    }
+
+    @Test
+    void testSetterValidation() {
+        PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer();
+        assertThrows(IllegalArgumentException.class, () -> renderer.setDpi(0));
+        assertThrows(IllegalArgumentException.class, () -> 
renderer.setMaxScaleTo(0));
+        assertThrows(IllegalArgumentException.class, () -> 
renderer.setMaxPixels(0));
+        assertThrows(IllegalArgumentException.class, () -> 
renderer.setMaxMemoryMb(-2));
+    }
+
+    @Test
+    void testRenderAllPages() throws Exception {
+        assumeTrue(hasPdfiumToPpm, "pdfiumtoppm not available");
+
+        PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer();
+
+        try (InputStream is = getClass().getResourceAsStream(
+                "/test-documents/testPDF_bookmarks.pdf")) {
+            assertNotNull(is, "test PDF not found");
+
+            try (TikaInputStream tis = TikaInputStream.get(is)) {
+                PageBasedRenderResults results =
+                        (PageBasedRenderResults) renderer.render(
+                                tis, new Metadata(), new ParseContext(),
+                                PageRangeRequest.RENDER_ALL);
+
+                List<RenderResult> allResults = results.getResults();
+                assertEquals(2, allResults.size(),
+                        "testPDF_bookmarks.pdf has 2 pages");
+
+                Set<Integer> pageNumbers = new HashSet<>();
+                for (RenderResult rr : allResults) {
+                    assertEquals(RenderResult.STATUS.SUCCESS, rr.getStatus());
+
+                    Metadata rm = rr.getMetadata();
+                    Integer page = rm.getInt(TikaPagedText.PAGE_NUMBER);
+                    assertNotNull(page, "page number should be set");
+                    pageNumbers.add(page);
+
+                    assertEquals(
+                            TikaCoreProperties.EmbeddedResourceType.RENDERING
+                                    .name(),
+                            rm.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+
+                    try (TikaInputStream imageTis = rr.getInputStream()) {
+                        byte[] imageBytes = imageTis.readAllBytes();
+                        assertTrue(imageBytes.length > 100,
+                                "rendered page should be a non-trivial PNG");
+                        assertEquals((byte) 0x89, imageBytes[0]);
+                        assertEquals((byte) 'P', imageBytes[1]);
+                        assertEquals((byte) 'N', imageBytes[2]);
+                        assertEquals((byte) 'G', imageBytes[3]);
+                    }
+                }
+
+                assertEquals(Set.of(1, 2), pageNumbers,
+                        "should have pages 1 and 2");
+
+                results.close();
+            }
+        }
+    }
+
+    @Test
+    void testRenderPageRange() throws Exception {
+        assumeTrue(hasPdfiumToPpm, "pdfiumtoppm not available");
+
+        PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer();
+
+        try (InputStream is = getClass().getResourceAsStream(
+                "/test-documents/testPDF_bookmarks.pdf")) {
+            assertNotNull(is, "test PDF not found");
+
+            try (TikaInputStream tis = TikaInputStream.get(is)) {
+                PageBasedRenderResults results =
+                        (PageBasedRenderResults) renderer.render(
+                                tis, new Metadata(), new ParseContext(),
+                                new PageRangeRequest(2, 2));
+
+                List<RenderResult> allResults = results.getResults();
+                assertEquals(1, allResults.size(),
+                        "should render exactly 1 page");
+
+                assertEquals(2,
+                        allResults.get(0).getMetadata()
+                                .getInt(TikaPagedText.PAGE_NUMBER));
+
+                results.close();
+            }
+        }
+    }
+
+    @Test
+    void testMemoryLimitHit() throws Exception {
+        assumeTrue(hasPdfiumToPpm, "pdfiumtoppm not available");
+
+        PdfiumToPpmRenderer renderer = new PdfiumToPpmRenderer();
+        // 300 dpi letter needs ~8 MB of bitmap on top of the ~64 MB baseline
+        renderer.setMaxMemoryMb(32);
+
+        try (InputStream is = getClass().getResourceAsStream(
+                "/test-documents/testPDF_bookmarks.pdf")) {
+            assertNotNull(is, "test PDF not found");
+            try (TikaInputStream tis = TikaInputStream.get(is)) {
+                assertThrows(TikaMemoryLimitException.class, () -> 
renderer.render(
+                        tis, new Metadata(), new ParseContext(),
+                        PageRangeRequest.RENDER_ALL));
+            }
+        }
+    }
+}

Reply via email to